@spark-apps/quickpeek 1.1.2 → 1.2.0

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("-");
@@ -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,123 +125,186 @@ var init_utils = __esm({
137
125
  });
138
126
 
139
127
  // src/lib/api-client.ts
140
- function hasCredentials() {
141
- return Boolean(process.env[ENV_SPARK_API_KEY]);
142
- }
143
- function getGroqApiKey() {
144
- return process.env[ENV_SPARK_API_KEY] || null;
145
- }
146
- function getStripeUrl() {
147
- return process.env[ENV_STRIPE_BACKEND] || DEFAULT_STRIPE_BACKEND;
148
- }
149
- async function getOrRefreshToken(apiKey) {
128
+ async function getOrRefreshDirectToken() {
150
129
  const now = Date.now();
151
- if (cachedToken && cachedToken.expiresAt > now + 3e5) {
152
- return cachedToken.token;
130
+ if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
131
+ return cachedDirectToken.token;
153
132
  }
154
133
  try {
155
134
  const response = await fetch(SPARK_AI_TOKEN, {
156
135
  method: "POST",
157
- headers: { "X-Api-Key": apiKey }
136
+ headers: { "X-Api-Key": API_KEY }
158
137
  });
138
+ if (!response.ok) {
139
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
140
+ }
159
141
  const data = await response.json();
160
- if (!response.ok || !data.success || !data.token) {
161
- throw new Error(data.error || `Token exchange failed: ${response.status}`);
142
+ if (!data.success || !data.token) {
143
+ throw new Error(data.error || `Token exchange failed: unexpected response`);
162
144
  }
163
- cachedToken = {
145
+ cachedDirectToken = {
164
146
  token: data.token,
165
147
  expiresAt: data.expiresAt || now + 36e5
166
- // Default 1 hour
167
148
  };
168
149
  return data.token;
169
150
  } catch (err) {
170
151
  throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
171
152
  }
172
153
  }
173
- async function callAI(systemPrompt, userPrompt, tier = "free") {
174
- const apiKey = getGroqApiKey();
175
- if (!apiKey) {
176
- return {
177
- success: false,
178
- error: `Missing API key. Set ${ENV_SPARK_API_KEY} environment variable.`,
179
- isAuthError: true
180
- };
154
+ async function getOrRefreshRelayToken(email) {
155
+ const now = Date.now();
156
+ if (cachedRelayToken && cachedRelayToken.email === email && cachedRelayToken.expiresAt > now + 3e5) {
157
+ return cachedRelayToken.token;
181
158
  }
182
159
  try {
183
- const token = await getOrRefreshToken(apiKey);
184
- let lastRateLimitError = null;
185
- for (let attempt = 1; attempt <= 3; attempt++) {
186
- try {
187
- const response = await fetch(SPARK_AI_CHAT, {
188
- method: "POST",
189
- headers: {
190
- "Content-Type": "application/json",
191
- Authorization: `Bearer ${token}`
192
- },
193
- body: JSON.stringify({
194
- messages: [
195
- { role: "system", content: systemPrompt },
196
- { role: "user", content: userPrompt }
197
- ]
198
- })
199
- });
200
- const data = await response.json();
201
- if (!response.ok) {
202
- const error = data.error || `Spark AI error: ${response.status}`;
203
- if (response.status === 429 && attempt < 3) {
204
- const retryAfter = response.headers.get("Retry-After");
205
- const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt - 1);
206
- lastRateLimitError = {
207
- success: false,
208
- error,
209
- isRateLimit: true,
210
- retryAfterSeconds: retryAfter ? parseInt(retryAfter, 10) : void 0
211
- };
212
- await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
213
- continue;
214
- }
215
- return {
160
+ const response = await fetch(RELAY_TOKEN_URL, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: JSON.stringify({ email })
164
+ });
165
+ const data = await response.json().catch(() => ({}));
166
+ if (!response.ok || !data.success || !data.token) {
167
+ throw new Error(data.error || `Token request failed: ${response.status} ${response.statusText}`);
168
+ }
169
+ cachedRelayToken = {
170
+ token: data.token,
171
+ expiresAt: data.expiresAt || now + 36e5,
172
+ email
173
+ };
174
+ return data.token;
175
+ } catch (err) {
176
+ throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
177
+ }
178
+ }
179
+ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
180
+ let lastRateLimitError = null;
181
+ for (let attempt = 1; attempt <= 3; attempt++) {
182
+ try {
183
+ const response = await fetch(chatUrl, {
184
+ method: "POST",
185
+ headers: {
186
+ "Content-Type": "application/json",
187
+ Authorization: `Bearer ${bearerToken}`
188
+ },
189
+ body: JSON.stringify({
190
+ messages: [
191
+ { role: "system", content: systemPrompt },
192
+ { role: "user", content: userPrompt }
193
+ ]
194
+ })
195
+ });
196
+ let data = {};
197
+ const contentType = response.headers.get("content-type") ?? "";
198
+ if (contentType.includes("application/json")) {
199
+ data = await response.json();
200
+ }
201
+ if (!response.ok) {
202
+ const error = data.error || `Spark AI error: ${response.status} ${response.statusText}`;
203
+ if (response.status === 429 && attempt < 3) {
204
+ const retryAfter = response.headers.get("Retry-After");
205
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * 2 ** (attempt - 1);
206
+ lastRateLimitError = {
216
207
  success: false,
217
208
  error,
218
- isRateLimit: response.status === 429,
219
- isAuthError: response.status === 401 || response.status === 403
220
- };
221
- }
222
- if (!data.success || !data.response) {
223
- return {
224
- success: false,
225
- error: data.error || "No response from Spark AI"
209
+ isRateLimit: true,
210
+ ...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
226
211
  };
212
+ await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
213
+ continue;
227
214
  }
228
215
  return {
229
- success: true,
230
- content: data.response,
231
- limits: {
232
- used: data.limits?.used ?? 0,
233
- limit: data.limits?.limit ?? 100,
234
- tier,
235
- resetAt: data.limits?.resetAt
236
- }
216
+ success: false,
217
+ error,
218
+ isRateLimit: response.status === 429,
219
+ isAuthError: response.status === 401 || response.status === 403
220
+ };
221
+ }
222
+ if (!data.success || !data.response) {
223
+ return {
224
+ success: false,
225
+ error: data.error || "No response from Spark AI"
237
226
  };
238
- } catch (err) {
239
- if (attempt === 3) throw err;
240
- const delayMs = 1e3 * Math.pow(2, attempt - 1);
241
- await new Promise((resolve9) => setTimeout(resolve9, delayMs));
242
227
  }
228
+ return {
229
+ success: true,
230
+ content: data.response,
231
+ limits: {
232
+ used: data.limits?.used ?? 0,
233
+ limit: data.limits?.limit ?? 100,
234
+ tier,
235
+ ...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
236
+ }
237
+ };
238
+ } catch (err) {
239
+ if (attempt === 3) throw err;
240
+ const delayMs = 1e3 * 2 ** (attempt - 1);
241
+ await new Promise((resolve9) => setTimeout(resolve9, delayMs));
243
242
  }
244
- return lastRateLimitError || { success: false, error: "Request failed after retries" };
243
+ }
244
+ return lastRateLimitError || { success: false, error: "Request failed after retries" };
245
+ }
246
+ async function callAI(systemPrompt, userPrompt, tier = "free") {
247
+ if (!API_KEY) {
248
+ return {
249
+ success: false,
250
+ error: "AI service is not configured. Please report this issue.",
251
+ isAuthError: true
252
+ };
253
+ }
254
+ try {
255
+ const token = await getOrRefreshDirectToken();
256
+ return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
257
+ } catch (err) {
258
+ return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
259
+ }
260
+ }
261
+ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
262
+ if (!email) {
263
+ return {
264
+ success: false,
265
+ error: "Sign in required. Run qp login to connect your account.",
266
+ isAuthError: true
267
+ };
268
+ }
269
+ try {
270
+ let token = await getOrRefreshRelayToken(email);
271
+ let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
272
+ if (!result.success && result.isAuthError) {
273
+ cachedRelayToken = null;
274
+ token = await getOrRefreshRelayToken(email);
275
+ result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
276
+ }
277
+ return result;
245
278
  } catch (err) {
246
279
  return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
247
280
  }
248
281
  }
249
- function getBackendUrls() {
250
- return { stripe: getStripeUrl() };
282
+ function parseAIPlanResponse(content) {
283
+ try {
284
+ let processedContent = content;
285
+ const codeBlockMatch = processedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
286
+ if (codeBlockMatch?.[1]) {
287
+ processedContent = codeBlockMatch[1].trim();
288
+ }
289
+ const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
290
+ if (!jsonMatch) {
291
+ return { success: false, error: "No valid JSON found in AI response" };
292
+ }
293
+ const parsed = JSON.parse(jsonMatch[0]);
294
+ return { success: true, plan: parsed };
295
+ } catch (err) {
296
+ return {
297
+ success: false,
298
+ error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
299
+ };
300
+ }
251
301
  }
252
- function getCredentials() {
253
- const key = getGroqApiKey();
254
- return key ? { apiKey: key } : null;
302
+ function normalizeUrl(input) {
303
+ let url = input.trim();
304
+ url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
305
+ return `https://${url}`;
255
306
  }
256
- var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, ENV_SPARK_API_KEY, ENV_STRIPE_BACKEND, DEFAULT_STRIPE_BACKEND, cachedToken;
307
+ var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, API_KEY, cachedDirectToken, cachedRelayToken;
257
308
  var init_api_client = __esm({
258
309
  "src/lib/api-client.ts"() {
259
310
  "use strict";
@@ -261,10 +312,91 @@ var init_api_client = __esm({
261
312
  SPARK_AI_BASE = "https://sparkbrain.app";
262
313
  SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
263
314
  SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
264
- ENV_SPARK_API_KEY = "SPARK_API_KEY";
265
- ENV_STRIPE_BACKEND = "QP_STRIPE_BACKEND";
266
- DEFAULT_STRIPE_BACKEND = "https://sparkstripe.com";
267
- cachedToken = null;
315
+ RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
316
+ RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
317
+ RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
318
+ API_KEY = process.env.SPARK_AI_KEY || "";
319
+ cachedDirectToken = null;
320
+ cachedRelayToken = null;
321
+ }
322
+ });
323
+
324
+ // src/lib/SparkStripe-feature-gating.ts
325
+ async function _fetch(email, init) {
326
+ const r = await fetch(
327
+ `${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`,
328
+ init
329
+ );
330
+ if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
331
+ return r.json();
332
+ }
333
+ async function getStatusCached(email) {
334
+ const k = email.toLowerCase(), hit = _cache.get(k);
335
+ if (hit && Date.now() < hit.exp) return hit.data;
336
+ const data = await getStatus(email);
337
+ _cache.set(k, { data, exp: Date.now() + 6e4 });
338
+ return data;
339
+ }
340
+ function getTier(d) {
341
+ if (!d.subscription || !d.registered) return "free";
342
+ if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
343
+ return TIERS[d.subscription.price_id] ?? "free";
344
+ }
345
+ async function getTierByEmail(email) {
346
+ try {
347
+ return getTier(await getStatusCached(email));
348
+ } catch {
349
+ return "free";
350
+ }
351
+ }
352
+ async function isVerified(email) {
353
+ try {
354
+ return (await getStatusCached(email)).verified;
355
+ } catch {
356
+ return false;
357
+ }
358
+ }
359
+ function pricingUrl(opts) {
360
+ const u = new URL(`${BASE}/pricing/${APP_ID}`);
361
+ if (opts?.email) u.searchParams.set("email", opts.email);
362
+ if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
363
+ return u.toString();
364
+ }
365
+ var APP_ID, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
366
+ var init_SparkStripe_feature_gating = __esm({
367
+ "src/lib/SparkStripe-feature-gating.ts"() {
368
+ "use strict";
369
+ APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
370
+ BASE = "https://sparkstripe.com";
371
+ TIERS = {
372
+ price_1TADnPEf9opkuWid9NtqOYON: "pro",
373
+ price_1TADnPEf9opkuWidgbvvX9ma: "pro",
374
+ price_1TADm7Ef9opkuWid3I9tquIB: "pro",
375
+ price_1TADm8Ef9opkuWid1RSA2wCa: "pro",
376
+ price_1TCREmEf9opkuWidvhuqzqDM: "pro",
377
+ price_1TCREmEf9opkuWidacf1tiC7: "pro"
378
+ };
379
+ RANK = {
380
+ free: 0,
381
+ pro: 1
382
+ };
383
+ FEATURES = {
384
+ free: [],
385
+ pro: [
386
+ "Everything in Free",
387
+ "No watermark",
388
+ "Full video composition (voiceover + captions + music + transitions)",
389
+ "Unlimited videos",
390
+ "Priority support"
391
+ ]
392
+ };
393
+ getStatus = (email) => _fetch(email);
394
+ _cache = /* @__PURE__ */ new Map();
395
+ isPaid = (d) => getTier(d) !== "free";
396
+ hasAccess = (d, min) => (RANK[getTier(d)] ?? 0) >= (RANK[min] ?? 0);
397
+ hasTrialRemaining = (d) => isPaid(d) || !!d.trial && !d.trial.is_expired && d.trial.uses_remaining > 0;
398
+ showPaywall = (d) => !isPaid(d) && !hasTrialRemaining(d);
399
+ isCanceling = (d) => !!d.subscription && d.subscription.cancel_at_period_end && d.subscription.status !== "canceled";
268
400
  }
269
401
  });
270
402
 
@@ -273,17 +405,12 @@ function shouldWatermark(tier, videoDurationSeconds) {
273
405
  return tier === "free" && videoDurationSeconds > 20;
274
406
  }
275
407
  function getUpgradeUrl(userEmail) {
276
- const stripe = getStripeUrl();
277
- let url = `${stripe}/pricing/quickpeek`;
278
- if (userEmail) {
279
- url += `?email=${encodeURIComponent(userEmail)}`;
280
- }
281
- return url;
408
+ return pricingUrl(userEmail ? { email: userEmail } : void 0);
282
409
  }
283
410
  var init_billing = __esm({
284
411
  "src/lib/billing.ts"() {
285
412
  "use strict";
286
- init_api_client();
413
+ init_SparkStripe_feature_gating();
287
414
  }
288
415
  });
289
416
 
@@ -340,7 +467,22 @@ var init_config = __esm({
340
467
  });
341
468
 
342
469
  // src/lib/scraping.ts
343
- var EXCLUDED_LINK_PATTERNS;
470
+ function isExcludedLink(text, href = "") {
471
+ const lowerText = text.toLowerCase();
472
+ const lowerHref = href.toLowerCase();
473
+ return EXCLUDED_LINK_PATTERNS.some((p) => lowerText.includes(p) || lowerHref.includes(p));
474
+ }
475
+ function shouldSkipLink(text, href, currentPath) {
476
+ if (!href || href === "#") return true;
477
+ try {
478
+ const linkPath = new URL(href).pathname;
479
+ if (linkPath === "/" || linkPath === currentPath) return true;
480
+ } catch {
481
+ return true;
482
+ }
483
+ return isExcludedLink(text, href);
484
+ }
485
+ var EXCLUDED_LINK_PATTERNS, INTERACTIVE_SELECTORS;
344
486
  var init_scraping = __esm({
345
487
  "src/lib/scraping.ts"() {
346
488
  "use strict";
@@ -407,6 +549,22 @@ var init_scraping = __esm({
407
549
  "feed",
408
550
  "home"
409
551
  ];
552
+ INTERACTIVE_SELECTORS = [
553
+ "button",
554
+ "input",
555
+ "select",
556
+ "textarea",
557
+ "a[href]",
558
+ '[role="button"]',
559
+ '[role="checkbox"]',
560
+ '[role="radio"]',
561
+ '[role="switch"]',
562
+ '[role="slider"]',
563
+ '[role="combobox"]',
564
+ '[role="menuitem"]',
565
+ '[type="submit"]',
566
+ "[onclick]"
567
+ ];
410
568
  }
411
569
  });
412
570
 
@@ -428,11 +586,17 @@ function getActionForType(type) {
428
586
  return "type";
429
587
  }
430
588
  }
431
- async function crawlPage(url, task2 = noopTask) {
589
+ async function crawlPage(url, task2 = noopTask, storageStatePath) {
590
+ const cached = crawlCache.get(url);
591
+ if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
592
+ task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
593
+ return { title: cached.title, elements: [...cached.elements] };
594
+ }
432
595
  const browserTask = task2("Launching browser");
433
596
  const browser = await import_playwright.chromium.launch({ headless: true });
434
- const page = await browser.newPage();
435
- browserTask.succeed("Browser launched");
597
+ const context = await browser.newContext(storageStatePath ? { storageState: storageStatePath } : {});
598
+ const page = await context.newPage();
599
+ browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
436
600
  const crawlTask = task2("Crawling page");
437
601
  await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
438
602
  const title = await page.title();
@@ -565,9 +729,14 @@ async function crawlPage(url, task2 = noopTask) {
565
729
  });
566
730
  await browser.close();
567
731
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
732
+ crawlCache.set(url, { timestamp: Date.now(), title, elements: [...elements] });
733
+ if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
734
+ const oldest = crawlCache.keys().next().value;
735
+ if (oldest !== void 0) crawlCache.delete(oldest);
736
+ }
568
737
  return { title, elements };
569
738
  }
570
- var import_playwright, noopTask;
739
+ var import_playwright, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
571
740
  var init_crawl = __esm({
572
741
  "src/lib/crawl.ts"() {
573
742
  "use strict";
@@ -575,6 +744,9 @@ var init_crawl = __esm({
575
744
  init_scraping();
576
745
  noopTask = () => ({ succeed: () => {
577
746
  } });
747
+ CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
748
+ CRAWL_CACHE_MAX_ENTRIES = 50;
749
+ crawlCache = /* @__PURE__ */ new Map();
578
750
  }
579
751
  });
580
752
 
@@ -619,10 +791,10 @@ var init_watermark = __esm({
619
791
  // src/lib/ffmpeg/spawn.ts
620
792
  function toWSLPath(path) {
621
793
  if (!isWindowsRuntime) return path;
622
- return windowsPathToWSL(path) ?? path.replace(/\\/g, "/");
794
+ return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path.replace(/\\/g, "/");
623
795
  }
624
796
  function convertArgToWSL(arg2) {
625
- const wslPath = windowsPathToWSL(arg2);
797
+ const wslPath = (0, import_video_kit2.windowsDrivePathToWsl)(arg2);
626
798
  if (wslPath) return wslPath;
627
799
  if (arg2.includes("\\")) return arg2.replace(/\\/g, "/");
628
800
  return arg2;
@@ -719,20 +891,24 @@ function composeVideo(opts) {
719
891
  `[1:a]apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}[voice]`,
720
892
  // Music: fade in and fade out
721
893
  `[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]`,
722
- `[voice][music]amix=inputs=2:duration=longest[aout]`
894
+ // Normalise the MIX and hard-limit its true peak - ported from
895
+ // VidLet, where single-pass loudnorm alone measured +0.9 dBTP on
896
+ // sharp TTS consonants. -14 LUFS is the YouTube/IG/TikTok playback
897
+ // target, so the platform leaves the audio untouched.
898
+ `[voice][music]amix=inputs=2:duration=longest,${import_video_kit3.MASTER_CHAIN}[aout]`
723
899
  ].join(";");
724
900
  args.push("-vf", vfStr);
725
901
  args.push("-filter_complex", complexFilter, "-map", "0:v", "-map", "[aout]");
726
902
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
727
903
  args.push("-c:a", "aac", "-b:a", audioBitrate);
728
904
  } else if (hasVoiceover) {
729
- const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn}`;
905
+ const af = `apad=whole_dur=${voiceoverDuration},afade=t=in:st=0:d=${fadeIn},${import_video_kit3.MASTER_CHAIN}`;
730
906
  args.push("-vf", vfStr);
731
907
  args.push("-map", "0:v", "-map", "1:a", "-af", af);
732
908
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
733
909
  args.push("-c:a", "aac", "-b:a", audioBitrate);
734
910
  } else if (hasMusic) {
735
- 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}`;
911
+ 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}`;
736
912
  args.push("-vf", vfStr);
737
913
  args.push("-map", "0:v", "-map", "1:a", "-af", musicFilter);
738
914
  args.push("-c:v", "libx264", "-preset", preset, "-crf", String(crf), "-pix_fmt", "yuv420p");
@@ -745,9 +921,11 @@ function composeVideo(opts) {
745
921
  runFFmpeg(args, "ffmpeg compose failed").then(resolve9, reject);
746
922
  });
747
923
  }
924
+ var import_video_kit3;
748
925
  var init_compose = __esm({
749
926
  "src/lib/ffmpeg/compose.ts"() {
750
927
  "use strict";
928
+ import_video_kit3 = require("@spark-apps/video-kit");
751
929
  init_watermark();
752
930
  init_spawn();
753
931
  }
@@ -1004,14 +1182,13 @@ function applyTransitions(opts) {
1004
1182
  filterParts.push(`[0:v]trim=start=${start.toFixed(3)}:end=${end.toFixed(3)},setpts=PTS-STARTPTS[v${i}]`);
1005
1183
  }
1006
1184
  let currentV = "v0";
1185
+ let chainDuration = boundaries[1] - boundaries[0];
1007
1186
  for (let i = 0; i < sorted.length; i++) {
1008
1187
  const trans = sorted[i];
1009
1188
  const duration = trans.duration ?? 0.3;
1010
1189
  const nextIdx = i + 1;
1011
- const segStart = boundaries[i];
1012
- const segEnd = boundaries[i + 1];
1013
- const segDuration = segEnd - segStart;
1014
- const offset = Math.max(0, segDuration - duration);
1190
+ const offset = Math.max(0, chainDuration - duration);
1191
+ chainDuration += boundaries[nextIdx + 1] - boundaries[nextIdx] - duration;
1015
1192
  const xfadeType = trans.type === "fade" ? "fadeblack" : "fade";
1016
1193
  const outV = i === sorted.length - 1 ? "outv" : `xv${i}`;
1017
1194
  filterParts.push(
@@ -1212,24 +1389,20 @@ var init_prompts = __esm({
1212
1389
 
1213
1390
  // src/lib/styles.ts
1214
1391
  function generateASSHeader(width, height, font, size, primaryColor, secondaryColor, outlineColor, backColor, bold, outlineWidth, shadow, marginBottom) {
1215
- return `[Script Info]
1216
- Title: QuickPeek Demo
1217
- ScriptType: v4.00+
1218
- PlayResX: ${width}
1219
- PlayResY: ${height}
1220
-
1221
- [V4+ Styles]
1222
- Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1223
- 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
1224
-
1225
- [Events]
1226
- Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
1227
- `;
1392
+ return (0, import_video_kit4.buildAssHeader)({
1393
+ title: "QuickPeek Demo",
1394
+ width,
1395
+ height,
1396
+ styles: [
1397
+ `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`
1398
+ ]
1399
+ });
1228
1400
  }
1229
- var CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
1401
+ var import_video_kit4, CURSOR_MOVE_DURATION, CURSOR_CSS, CURSOR_SCRIPT;
1230
1402
  var init_styles = __esm({
1231
1403
  "src/lib/styles.ts"() {
1232
1404
  "use strict";
1405
+ import_video_kit4 = require("@spark-apps/video-kit");
1233
1406
  init_cursor();
1234
1407
  CURSOR_MOVE_DURATION = 800;
1235
1408
  CURSOR_CSS = `
@@ -1273,49 +1446,13 @@ var init_styles = __esm({
1273
1446
  });
1274
1447
 
1275
1448
  // src/lib/subscription-client.ts
1276
- async function fetchStatus(email) {
1277
- const cacheKey = `${email}:${APP_ID}`;
1278
- const cached = responseCache.get(cacheKey);
1279
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.data;
1280
- const stripe = getStripeUrl();
1281
- try {
1282
- const params = new URLSearchParams({ email, app_id: APP_ID });
1283
- const response = await fetch(`${stripe}/api/subscription/status-public?${params}`);
1284
- if (!response.ok) return null;
1285
- const data = await response.json();
1286
- responseCache.set(cacheKey, { data, timestamp: Date.now() });
1287
- return data;
1288
- } catch {
1289
- return null;
1290
- }
1291
- }
1292
- function resolveTier(status) {
1293
- return status && PRO_STATUSES.includes(status) ? "pro" : "free";
1294
- }
1295
- async function getTier(userEmail) {
1296
- const data = await fetchStatus(userEmail);
1297
- if (!data) return "free";
1298
- const status = data.subscription?.status;
1299
- const tier = resolveTier(status);
1300
- if (tier === "pro" && status === "trialing" && data.subscription?.current_period_end) {
1301
- if (new Date(data.subscription.current_period_end) <= /* @__PURE__ */ new Date()) return "free";
1302
- }
1303
- return tier;
1304
- }
1305
- async function checkVerificationStatus(email) {
1306
- const data = await fetchStatus(email);
1307
- if (!data) return { verified: false, tier: "free" };
1308
- const verified = data.verified === true || data.user?.verified === true || data.user?.email_verified === true;
1309
- const tier = resolveTier(data.subscription?.status);
1310
- return { verified, tier };
1311
- }
1312
1449
  async function registerFreeUser(email) {
1313
- const stripe = getStripeUrl();
1450
+ const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
1314
1451
  try {
1315
- const response = await fetch(`${stripe}/api/register-free`, {
1452
+ const response = await fetch(`https://sparkstripe.com/api/register-free`, {
1316
1453
  method: "POST",
1317
1454
  headers: { "Content-Type": "application/json" },
1318
- body: JSON.stringify({ email, app_id: APP_ID, app_name: "QuickPeek" })
1455
+ body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
1319
1456
  });
1320
1457
  const text = await response.text();
1321
1458
  let data = {};
@@ -1340,21 +1477,15 @@ async function registerFreeUser(email) {
1340
1477
  return { success: false, error: err instanceof Error ? err.message : "Network error" };
1341
1478
  }
1342
1479
  }
1343
- var APP_ID, PRO_STATUSES, CACHE_TTL_MS, responseCache;
1344
1480
  var init_subscription_client = __esm({
1345
1481
  "src/lib/subscription-client.ts"() {
1346
1482
  "use strict";
1347
- init_api_client();
1348
- APP_ID = "quickpeek";
1349
- PRO_STATUSES = ["active", "trialing", "lifetime"];
1350
- CACHE_TTL_MS = 6e4;
1351
- responseCache = /* @__PURE__ */ new Map();
1352
1483
  }
1353
1484
  });
1354
1485
 
1355
1486
  // src/lib/tts/helpers.ts
1356
1487
  function toWSLPathForTTS(path) {
1357
- return windowsPathToWSL(path) ?? path;
1488
+ return (0, import_video_kit2.windowsDrivePathToWsl)(path) ?? path;
1358
1489
  }
1359
1490
  function estimateActionDuration(step) {
1360
1491
  if (step.action === "drag" && step.value === "around") {
@@ -1384,8 +1515,8 @@ async function getMediaStepDuration(step, baseDir) {
1384
1515
  if (!["mp4", "webm", "mov"].includes(ext)) {
1385
1516
  return 0;
1386
1517
  }
1387
- const mediaPath = (0, import_node_path.isAbsolute)(step.value) ? step.value : (0, import_node_path.resolve)(baseDir, step.value);
1388
- if (!(0, import_node_fs.existsSync)(mediaPath)) {
1518
+ const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
1519
+ if (!(0, import_node_fs2.existsSync)(mediaPath)) {
1389
1520
  return 0;
1390
1521
  }
1391
1522
  try {
@@ -1405,38 +1536,38 @@ async function getMediaStepDuration(step, baseDir) {
1405
1536
  }
1406
1537
  }
1407
1538
  async function loadVoiceoverMeta(outputDir) {
1408
- const metaPath = (0, import_node_path.join)(outputDir, "voiceover.json");
1539
+ const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1409
1540
  try {
1410
- if (!(0, import_node_fs.existsSync)(metaPath)) return null;
1411
- const content = await (0, import_promises.readFile)(metaPath, "utf-8");
1541
+ if (!(0, import_node_fs2.existsSync)(metaPath)) return null;
1542
+ const content = await (0, import_promises2.readFile)(metaPath, "utf-8");
1412
1543
  return JSON.parse(content);
1413
1544
  } catch {
1414
1545
  return null;
1415
1546
  }
1416
1547
  }
1417
1548
  async function saveVoiceoverMeta(outputDir, meta) {
1418
- const metaPath = (0, import_node_path.join)(outputDir, "voiceover.json");
1419
- await (0, import_promises.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1549
+ const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
1550
+ await (0, import_promises2.writeFile)(metaPath, JSON.stringify(meta, null, 2));
1420
1551
  }
1421
1552
  async function concatenateAudio(inputs, output, workDir) {
1422
1553
  const formatPath = (f) => {
1423
- const resolved = (0, import_node_path.resolve)(f);
1424
- return isWindows ? windowsPathToWSL(resolved) ?? resolved : resolved;
1554
+ const resolved = (0, import_node_path2.resolve)(f);
1555
+ return isWindows ? (0, import_video_kit2.windowsDrivePathToWsl)(resolved) ?? resolved : resolved;
1425
1556
  };
1426
1557
  const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
1427
- const listPath = (0, import_node_path.join)(workDir, "concat.txt");
1428
- await (0, import_promises.writeFile)(listPath, listContent);
1558
+ const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
1559
+ await (0, import_promises2.writeFile)(listPath, listContent);
1429
1560
  await concatMedia(listPath, output);
1430
- await (0, import_promises.unlink)(listPath).catch(() => {
1561
+ await (0, import_promises2.unlink)(listPath).catch(() => {
1431
1562
  });
1432
1563
  }
1433
- var import_node_fs, import_promises, import_node_path, isWindows;
1564
+ var import_node_fs2, import_promises2, import_node_path2, isWindows;
1434
1565
  var init_helpers = __esm({
1435
1566
  "src/lib/tts/helpers.ts"() {
1436
1567
  "use strict";
1437
- import_node_fs = require("fs");
1438
- import_promises = require("fs/promises");
1439
- import_node_path = require("path");
1568
+ import_node_fs2 = require("fs");
1569
+ import_promises2 = require("fs/promises");
1570
+ import_node_path2 = require("path");
1440
1571
  init_ffmpeg2();
1441
1572
  init_utils();
1442
1573
  isWindows = process.platform === "win32";
@@ -1472,7 +1603,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1472
1603
  if (!needsRealignment) {
1473
1604
  return stepAudio;
1474
1605
  }
1475
- const audioDir = (0, import_node_path2.join)(workDir, "audio");
1606
+ const audioDir = (0, import_node_path3.join)(workDir, "audio");
1476
1607
  const segments = [];
1477
1608
  const alignedStepAudio = /* @__PURE__ */ new Map();
1478
1609
  for (let i = 0; i < actualRecordingDurations.length; i++) {
@@ -1481,20 +1612,20 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1481
1612
  const voiceDuration = voiceData?.duration ?? 0;
1482
1613
  const recordingDuration = actualRecordingDurations[i] ?? 0;
1483
1614
  const targetDuration = Math.max(voiceDuration, recordingDuration);
1484
- const audioPath = (0, import_node_path2.join)(audioDir, `step-${stepNum}.mp3`);
1485
- const originalPaddingPath = (0, import_node_path2.join)(audioDir, `step-${stepNum}-padding.mp3`);
1486
- const silencePath = (0, import_node_path2.join)(audioDir, `step-${stepNum}-silence.mp3`);
1487
- if ((0, import_node_fs2.existsSync)(audioPath)) {
1615
+ const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
1616
+ const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
1617
+ const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
1618
+ if ((0, import_node_fs3.existsSync)(audioPath)) {
1488
1619
  segments.push(audioPath);
1489
- if ((0, import_node_fs2.existsSync)(originalPaddingPath)) {
1620
+ if ((0, import_node_fs3.existsSync)(originalPaddingPath)) {
1490
1621
  segments.push(originalPaddingPath);
1491
1622
  }
1492
1623
  if (recordingDuration > voiceDuration) {
1493
- const alignPaddingPath = (0, import_node_path2.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
1624
+ const alignPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-align-padding.mp3`);
1494
1625
  await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
1495
1626
  segments.push(alignPaddingPath);
1496
1627
  }
1497
- } else if ((0, import_node_fs2.existsSync)(silencePath)) {
1628
+ } else if ((0, import_node_fs3.existsSync)(silencePath)) {
1498
1629
  if (targetDuration !== voiceDuration) {
1499
1630
  await generateSilence(silencePath, targetDuration);
1500
1631
  }
@@ -1506,7 +1637,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1506
1637
  });
1507
1638
  }
1508
1639
  if (segments.length > 0) {
1509
- const voiceoverPath = (0, import_node_path2.join)(outputDir, "voiceover.wav");
1640
+ const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
1510
1641
  await concatenateAudio(segments, voiceoverPath, workDir);
1511
1642
  }
1512
1643
  const existingMeta = await loadVoiceoverMeta(outputDir);
@@ -1518,12 +1649,12 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
1518
1649
  }
1519
1650
  return alignedStepAudio;
1520
1651
  }
1521
- var import_node_fs2, import_node_path2;
1652
+ var import_node_fs3, import_node_path3;
1522
1653
  var init_timing = __esm({
1523
1654
  "src/lib/tts/timing.ts"() {
1524
1655
  "use strict";
1525
- import_node_fs2 = require("fs");
1526
- import_node_path2 = require("path");
1656
+ import_node_fs3 = require("fs");
1657
+ import_node_path3 = require("path");
1527
1658
  init_ffmpeg2();
1528
1659
  init_helpers();
1529
1660
  }
@@ -1554,7 +1685,7 @@ function canReuseVoiceover(steps, voice, existingMeta) {
1554
1685
  }
1555
1686
  async function loadExistingVoiceover(steps, outputDir, meta, config) {
1556
1687
  const { estimateWordTimings: estimateWordTimings2 } = await Promise.resolve().then(() => (init_timing(), timing_exports));
1557
- const voiceoverPath = (0, import_node_path3.join)(outputDir, "voiceover.wav");
1688
+ const voiceoverPath = (0, import_node_path4.join)(outputDir, "voiceover.wav");
1558
1689
  const stepAudio = /* @__PURE__ */ new Map();
1559
1690
  for (let i = 0; i < steps.length; i++) {
1560
1691
  const step = steps[i];
@@ -1567,97 +1698,29 @@ async function loadExistingVoiceover(steps, outputDir, meta, config) {
1567
1698
  }
1568
1699
  return { voiceoverPath, stepAudio };
1569
1700
  }
1570
- var import_node_path3;
1701
+ var import_node_path4;
1571
1702
  var init_cache = __esm({
1572
1703
  "src/lib/tts/cache.ts"() {
1573
1704
  "use strict";
1574
- import_node_path3 = require("path");
1705
+ import_node_path4 = require("path");
1575
1706
  init_helpers();
1576
1707
  }
1577
1708
  });
1578
1709
 
1579
1710
  // src/lib/voices.ts
1580
- var LANGUAGE_VOICES;
1711
+ var import_video_kit5;
1581
1712
  var init_voices = __esm({
1582
1713
  "src/lib/voices.ts"() {
1583
1714
  "use strict";
1584
- LANGUAGE_VOICES = {
1585
- en: "en-GB-SoniaNeural",
1586
- ar: "ar-LB-LaylaNeural",
1587
- es: "es-ES-ElviraNeural",
1588
- fr: "fr-FR-DeniseNeural",
1589
- de: "de-DE-KatjaNeural",
1590
- it: "it-IT-ElsaNeural",
1591
- pt: "pt-BR-FranciscaNeural",
1592
- ru: "ru-RU-SvetlanaNeural",
1593
- zh: "zh-CN-XiaoxiaoNeural",
1594
- ja: "ja-JP-NanamiNeural",
1595
- ko: "ko-KR-SunHiNeural",
1596
- hi: "hi-IN-SwaraNeural",
1597
- tr: "tr-TR-EmelNeural",
1598
- nl: "nl-NL-ColetteNeural",
1599
- pl: "pl-PL-AgnieszkaNeural",
1600
- sv: "sv-SE-SofieNeural",
1601
- he: "he-IL-HilaNeural",
1602
- th: "th-TH-PremwadeeNeural",
1603
- vi: "vi-VN-HoaiMyNeural",
1604
- id: "id-ID-GadisNeural",
1605
- ms: "ms-MY-YasminNeural",
1606
- fil: "fil-PH-BlessicaNeural",
1607
- uk: "uk-UA-PolinaNeural",
1608
- cs: "cs-CZ-VlastaNeural",
1609
- el: "el-GR-AthinaNeural",
1610
- ro: "ro-RO-AlinaNeural",
1611
- hu: "hu-HU-NoemiNeural",
1612
- da: "da-DK-ChristelNeural",
1613
- fi: "fi-FI-NooraNeural",
1614
- no: "nb-NO-PernilleNeural",
1615
- sk: "sk-SK-ViktoriaNeural",
1616
- bg: "bg-BG-KalinaNeural",
1617
- hr: "hr-HR-GabrijelaNeural",
1618
- sl: "sl-SI-PetraNeural",
1619
- et: "et-EE-AnuNeural",
1620
- lv: "lv-LV-EveritaNeural",
1621
- lt: "lt-LT-OnaNeural",
1622
- bn: "bn-IN-TanishaaNeural",
1623
- ta: "ta-IN-PallaviNeural",
1624
- te: "te-IN-ShrutiNeural",
1625
- mr: "mr-IN-AarohiNeural",
1626
- gu: "gu-IN-DhwaniNeural",
1627
- kn: "kn-IN-SapnaNeural",
1628
- ml: "ml-IN-SobhanaNeural",
1629
- ur: "ur-PK-UzmaNeural",
1630
- fa: "fa-IR-DilaraNeural",
1631
- sw: "sw-KE-ZuriNeural",
1632
- af: "af-ZA-AdriNeural",
1633
- ca: "ca-ES-JoanaNeural",
1634
- cy: "cy-GB-NiaNeural",
1635
- ga: "ga-IE-OrlaNeural",
1636
- mt: "mt-MT-GraceNeural",
1637
- is: "is-IS-GudrunNeural",
1638
- mk: "mk-MK-MarijaNeural",
1639
- sr: "sr-RS-SophieNeural",
1640
- bs: "bs-BA-VesnaNeural",
1641
- sq: "sq-AL-AnilaNeural",
1642
- am: "am-ET-MekdesNeural",
1643
- az: "az-AZ-BabekNeural",
1644
- ka: "ka-GE-EkaNeural",
1645
- kk: "kk-KZ-AigulNeural",
1646
- mn: "mn-MN-YesUINeural",
1647
- ne: "ne-NP-HemkalaNeural",
1648
- ps: "ps-AF-LatifaNeural",
1649
- si: "si-LK-ThiliniNeural",
1650
- so: "so-SO-UbaxNeural",
1651
- zu: "zu-ZA-ThandoNeural"
1652
- };
1715
+ import_video_kit5 = require("@spark-apps/video-kit");
1653
1716
  }
1654
1717
  });
1655
1718
 
1656
1719
  // src/lib/tts/synthesize.ts
1657
1720
  async function synthesizeSpeech(text, outputPath, voice) {
1658
- const tempScriptPath = (0, import_node_path4.join)((0, import_node_path4.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
1659
- const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path4.resolve)(tempScriptPath)) : tempScriptPath;
1660
- const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path4.resolve)(outputPath)) : outputPath;
1721
+ const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
1722
+ const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
1723
+ const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
1661
1724
  const pythonScript = `
1662
1725
  import asyncio
1663
1726
  from edge_tts import Communicate
@@ -1672,7 +1735,7 @@ async def main():
1672
1735
  asyncio.run(main())
1673
1736
  `;
1674
1737
  try {
1675
- await (0, import_promises2.writeFile)(tempScriptPath, pythonScript);
1738
+ await (0, import_promises3.writeFile)(tempScriptPath, pythonScript);
1676
1739
  const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
1677
1740
  await execAsync(pythonCmd, { timeout: 3e4 });
1678
1741
  await import("fs/promises").then((fs) => fs.unlink(tempScriptPath).catch(() => {
@@ -1700,13 +1763,13 @@ asyncio.run(main())
1700
1763
  }
1701
1764
  }
1702
1765
  }
1703
- var import_node_child_process2, import_promises2, import_node_path4, import_node_util, execAsync;
1766
+ var import_node_child_process2, import_promises3, import_node_path5, import_node_util, execAsync;
1704
1767
  var init_synthesize = __esm({
1705
1768
  "src/lib/tts/synthesize.ts"() {
1706
1769
  "use strict";
1707
1770
  import_node_child_process2 = require("child_process");
1708
- import_promises2 = require("fs/promises");
1709
- import_node_path4 = require("path");
1771
+ import_promises3 = require("fs/promises");
1772
+ import_node_path5 = require("path");
1710
1773
  import_node_util = require("util");
1711
1774
  init_helpers();
1712
1775
  execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
@@ -1720,7 +1783,7 @@ function estimateDurationFromText(text, wordsPerMinute = 150) {
1720
1783
  }
1721
1784
  function resolveVoice(config) {
1722
1785
  if (config.audio?.voice) return config.audio.voice;
1723
- if (config.lang) return LANGUAGE_VOICES[config.lang] || null;
1786
+ if (config.lang) return import_video_kit5.WIDE_VOICES[config.lang] || null;
1724
1787
  return null;
1725
1788
  }
1726
1789
  async function generateVoiceover(steps, config, workDir, outputDir, logError2, task2, info2) {
@@ -1745,8 +1808,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1745
1808
  info2("No voice configured");
1746
1809
  return { voiceoverPath: "", stepAudio: stepAudio2 };
1747
1810
  }
1748
- const audioDir = (0, import_node_path5.join)(workDir, "audio");
1749
- await (0, import_promises3.mkdir)(audioDir, { recursive: true });
1811
+ const audioDir = (0, import_node_path6.join)(workDir, "audio");
1812
+ await (0, import_promises4.mkdir)(audioDir, { recursive: true });
1750
1813
  const segments = [];
1751
1814
  const stepAudio = /* @__PURE__ */ new Map();
1752
1815
  const ttsTask = task2("Generating voiceover");
@@ -1758,13 +1821,13 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1758
1821
  const manualDuration = (step.duration ?? 0) * 1e3;
1759
1822
  const mediaDuration = await getMediaStepDuration(step, outputDir);
1760
1823
  const silenceDuration = Math.max(actionDuration, manualDuration, mediaDuration, config.timing.minStepDuration);
1761
- const silencePath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-silence.mp3`);
1824
+ const silencePath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-silence.mp3`);
1762
1825
  await generateSilence(silencePath, silenceDuration);
1763
1826
  segments.push(silencePath);
1764
1827
  stepAudio.set(stepNum, { duration: silenceDuration, words: [] });
1765
1828
  continue;
1766
1829
  }
1767
- const audioPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}.mp3`);
1830
+ const audioPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}.mp3`);
1768
1831
  if (segments.length > 0) await new Promise((r) => setTimeout(r, 200));
1769
1832
  try {
1770
1833
  await synthesizeSpeech(step.caption, audioPath, voice);
@@ -1781,7 +1844,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1781
1844
  mediaDuration
1782
1845
  );
1783
1846
  if (stepDuration > audioDuration) {
1784
- const paddingPath = (0, import_node_path5.join)(audioDir, `step-${stepNum}-padding.mp3`);
1847
+ const paddingPath = (0, import_node_path6.join)(audioDir, `step-${stepNum}-padding.mp3`);
1785
1848
  await generateSilence(paddingPath, stepDuration - audioDuration);
1786
1849
  segments.push(paddingPath);
1787
1850
  }
@@ -1797,8 +1860,8 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1797
1860
  return { voiceoverPath: "", stepAudio };
1798
1861
  }
1799
1862
  const concatTask = task2("Combining audio");
1800
- await (0, import_promises3.mkdir)(outputDir, { recursive: true });
1801
- const voiceoverPath = (0, import_node_path5.join)(outputDir, "voiceover.wav");
1863
+ await (0, import_promises4.mkdir)(outputDir, { recursive: true });
1864
+ const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
1802
1865
  await concatenateAudio(segments, voiceoverPath, workDir);
1803
1866
  concatTask.succeed("Done");
1804
1867
  const meta = {
@@ -1811,12 +1874,12 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
1811
1874
  await saveVoiceoverMeta(outputDir, meta);
1812
1875
  return { voiceoverPath, stepAudio };
1813
1876
  }
1814
- var import_promises3, import_node_path5;
1877
+ var import_promises4, import_node_path6;
1815
1878
  var init_generate = __esm({
1816
1879
  "src/lib/tts/generate.ts"() {
1817
1880
  "use strict";
1818
- import_promises3 = require("fs/promises");
1819
- import_node_path5 = require("path");
1881
+ import_promises4 = require("fs/promises");
1882
+ import_node_path6 = require("path");
1820
1883
  init_ffmpeg2();
1821
1884
  init_voices();
1822
1885
  init_helpers();
@@ -1869,7 +1932,9 @@ __export(lib_exports, {
1869
1932
  DEFAULT_CAPTIONS: () => DEFAULT_CAPTIONS,
1870
1933
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
1871
1934
  EXCLUDED_LINK_PATTERNS: () => EXCLUDED_LINK_PATTERNS,
1872
- LANGUAGE_VOICES: () => LANGUAGE_VOICES,
1935
+ FEATURES: () => FEATURES,
1936
+ INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
1937
+ LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
1873
1938
  VERSION: () => VERSION,
1874
1939
  alignVoiceoverToRecording: () => alignVoiceoverToRecording,
1875
1940
  applyTransitions: () => applyTransitions,
@@ -1879,8 +1944,8 @@ __export(lib_exports, {
1879
1944
  buildUserPrompt: () => buildUserPrompt,
1880
1945
  buildWatermarkFilters: () => buildWatermarkFilters,
1881
1946
  callAI: () => callAI,
1947
+ callAIViaRelay: () => callAIViaRelay,
1882
1948
  canReuseVoiceover: () => canReuseVoiceover,
1883
- checkVerificationStatus: () => checkVerificationStatus,
1884
1949
  composeVideo: () => composeVideo,
1885
1950
  concatMedia: () => concatMedia,
1886
1951
  concatVideoWithOutro: () => concatVideoWithOutro,
@@ -1889,30 +1954,38 @@ __export(lib_exports, {
1889
1954
  generateASSHeader: () => generateASSHeader,
1890
1955
  generateSilence: () => generateSilence,
1891
1956
  generateVoiceover: () => generateVoiceover,
1892
- getBackendUrls: () => getBackendUrls,
1893
- getCredentials: () => getCredentials,
1894
1957
  getErrorMessage: () => getErrorMessage,
1895
- getGroqApiKey: () => getGroqApiKey,
1896
1958
  getLanguageName: () => getLanguageName,
1897
1959
  getMediaDuration: () => getMediaDuration,
1898
- getStripeUrl: () => getStripeUrl,
1899
- getTier: () => getTier,
1960
+ getStatus: () => getStatus,
1961
+ getStatusCached: () => getStatusCached,
1962
+ getTierByEmail: () => getTierByEmail,
1900
1963
  getUpgradeUrl: () => getUpgradeUrl,
1901
- hasCredentials: () => hasCredentials,
1902
- hexToASS: () => hexToASS,
1964
+ hasAccess: () => hasAccess,
1965
+ hasTrialRemaining: () => hasTrialRemaining,
1966
+ hexToASS: () => import_video_kit.hexToAss,
1967
+ isCanceling: () => isCanceling,
1968
+ isExcludedLink: () => isExcludedLink,
1969
+ isPaid: () => isPaid,
1970
+ isVerified: () => isVerified,
1903
1971
  loadExistingVoiceover: () => loadExistingVoiceover,
1904
1972
  loadVoiceoverMeta: () => loadVoiceoverMeta,
1905
1973
  mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
1974
+ normalizeUrl: () => normalizeUrl,
1975
+ parseAIPlanResponse: () => parseAIPlanResponse,
1906
1976
  parsePartial: () => parsePartial,
1977
+ pricingUrl: () => pricingUrl,
1907
1978
  registerFreeUser: () => registerFreeUser,
1908
1979
  resolveVoice: () => resolveVoice,
1909
1980
  rgbaToHex: () => rgbaToHex,
1981
+ shouldSkipLink: () => shouldSkipLink,
1910
1982
  shouldWatermark: () => shouldWatermark,
1983
+ showPaywall: () => showPaywall,
1911
1984
  speedUpVideo: () => speedUpVideo,
1912
1985
  spliceVideos: () => spliceVideos,
1913
1986
  synthesizeSpeech: () => synthesizeSpeech,
1914
1987
  toTitleCase: () => toTitleCase,
1915
- windowsPathToWSL: () => windowsPathToWSL
1988
+ windowsPathToWSL: () => import_video_kit2.windowsDrivePathToWsl
1916
1989
  });
1917
1990
  var init_lib = __esm({
1918
1991
  "src/lib/index.ts"() {
@@ -1924,6 +1997,7 @@ var init_lib = __esm({
1924
1997
  init_cursor();
1925
1998
  init_ffmpeg2();
1926
1999
  init_prompts();
2000
+ init_SparkStripe_feature_gating();
1927
2001
  init_scraping();
1928
2002
  init_styles();
1929
2003
  init_subscription_client();
@@ -1937,134 +2011,692 @@ var init_lib = __esm({
1937
2011
 
1938
2012
  // src/qp.ts
1939
2013
  var import_dotenv = __toESM(require("dotenv"));
1940
- var import_chalk16 = __toESM(require("chalk"));
2014
+ var import_chalk20 = __toESM(require("chalk"));
1941
2015
 
1942
- // src/commands/capture-cmd.ts
1943
- var import_node_path19 = require("path");
1944
- var import_chalk7 = __toESM(require("chalk"));
1945
-
1946
- // src/ai/narrate.ts
1947
- init_lib();
2016
+ // src/commands/auth-cmd.ts
2017
+ var import_node_readline2 = require("readline");
2018
+ var import_chalk3 = __toESM(require("chalk"));
2019
+ var import_playwright2 = require("playwright");
1948
2020
 
1949
- // src/ai/client.ts
1950
- var import_node_readline = require("readline");
1951
- var import_chalk = __toESM(require("chalk"));
1952
- init_lib();
1953
- var lastRateLimitInfo = null;
1954
- function getLastRateLimitInfo() {
1955
- return lastRateLimitInfo;
2021
+ // src/browser/auth-state.ts
2022
+ var import_node_fs = require("fs");
2023
+ var import_promises = require("fs/promises");
2024
+ var import_node_os = require("os");
2025
+ var import_node_path = require("path");
2026
+ var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
2027
+ function hostOf(url) {
2028
+ try {
2029
+ return new URL(url).host;
2030
+ } catch {
2031
+ return null;
2032
+ }
1956
2033
  }
1957
- function prompt(question) {
1958
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
1959
- return new Promise((resolve9) => {
1960
- rl.question(question, (answer) => {
1961
- rl.close();
1962
- resolve9(answer.trim());
1963
- });
1964
- });
2034
+ function authStatePath(url) {
2035
+ const host = hostOf(url);
2036
+ return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
1965
2037
  }
1966
- async function setupUserEmail() {
1967
- console.log();
1968
- console.log(import_chalk.default.yellow(" \u2709 Account Setup"));
1969
- console.log(import_chalk.default.dim(" \u2500".repeat(20)));
1970
- console.log();
1971
- console.log(import_chalk.default.white(" QuickPeek uses your email to track usage and"));
1972
- console.log(import_chalk.default.white(" unlock Pro features when you subscribe."));
1973
- console.log();
1974
- const email = await prompt(import_chalk.default.white(" Email: "));
1975
- if (!email) {
1976
- console.log();
1977
- console.log(import_chalk.default.red(" No email provided. Exiting."));
1978
- process.exit(1);
1979
- }
1980
- if (!email.includes("@") || !email.includes(".")) {
1981
- console.log();
1982
- console.log(import_chalk.default.yellow(" \u26A0 That doesn't look like a valid email address."));
1983
- const proceed = await prompt(import_chalk.default.white(" Continue anyway? ") + import_chalk.default.dim("[y/N] "));
1984
- if (proceed.toLowerCase() !== "y") {
1985
- process.exit(1);
1986
- }
1987
- }
1988
- console.log();
1989
- console.log(import_chalk.default.green(" \u2713 Email registered"));
1990
- console.log();
1991
- return email;
2038
+ function findAuthState(url) {
2039
+ const path = authStatePath(url);
2040
+ return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
1992
2041
  }
1993
- function checkCredentials() {
1994
- if (!hasCredentials()) {
1995
- console.log();
1996
- console.log(import_chalk.default.red(" \u26A0 Spark AI API key not configured"));
1997
- console.log();
1998
- console.log(import_chalk.default.dim(" Set the following environment variable:"));
1999
- console.log(import_chalk.default.white(" SPARK_API_KEY=your-spark-api-key"));
2000
- console.log();
2001
- console.log(import_chalk.default.dim(" Get a key at: https://sparkbrain.app/dashboard"));
2002
- console.log();
2003
- process.exit(1);
2004
- }
2042
+ async function saveAuthState(url, state) {
2043
+ const path = authStatePath(url);
2044
+ if (!path) throw new Error(`Not a valid URL: ${url}`);
2045
+ await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
2046
+ await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
2047
+ await (0, import_promises.chmod)(path, 384);
2048
+ return path;
2005
2049
  }
2006
- async function callGroq(systemPrompt, userPrompt, logError2, _userEmail, tier = "free") {
2007
- const result = await callAI(systemPrompt, userPrompt, tier);
2008
- if (!result.success) {
2009
- await logError2("AI API", new Error(result.error));
2010
- if (result.isRateLimit) {
2011
- console.log();
2012
- console.log(import_chalk.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
2013
- console.log();
2014
- console.log(import_chalk.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
2015
- console.log(import_chalk.default.dim(" Options:"));
2016
- if (result.retryAfterSeconds) {
2017
- const minutes = Math.ceil(result.retryAfterSeconds / 60);
2018
- console.log(import_chalk.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
2019
- } else {
2020
- console.log(import_chalk.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
2021
- }
2022
- console.log(import_chalk.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
2023
- console.log(import_chalk.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
2024
- console.log();
2025
- process.exit(1);
2026
- }
2027
- if (result.isAuthError) {
2028
- console.log();
2029
- console.log(import_chalk.default.red(" \u26A0 Authentication Error"));
2030
- console.log();
2031
- console.log(import_chalk.default.dim(` ${result.error}`));
2032
- console.log();
2033
- process.exit(1);
2034
- }
2035
- console.log();
2036
- console.log(import_chalk.default.red(" \u26A0 AI Error"));
2037
- console.log();
2038
- const firstLine = result.error.split("\n")[0] ?? result.error;
2039
- console.log(import_chalk.default.dim(` ${firstLine.slice(0, 80)}`));
2040
- console.log();
2041
- process.exit(1);
2050
+ async function clearAuthState(url) {
2051
+ if (url) {
2052
+ const path = authStatePath(url);
2053
+ if (!path || !(0, import_node_fs.existsSync)(path)) return [];
2054
+ await (0, import_promises.unlink)(path);
2055
+ return [hostOf(url)];
2042
2056
  }
2043
- lastRateLimitInfo = result.limits;
2044
- return result.content;
2057
+ if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2058
+ const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
2059
+ await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
2060
+ return files.map((f) => f.slice(0, -".json".length));
2061
+ }
2062
+ async function listAuthStates() {
2063
+ if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
2064
+ return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
2045
2065
  }
2046
2066
 
2047
- // src/ai/narrate.ts
2048
- async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
2049
- const langName = getLanguageName(lang);
2050
- const stepList = steps.map((s, i) => {
2051
- const parts = [`${i + 1}. ${s.action}`];
2052
- if (s.target) parts.push(`on "${s.target}"`);
2053
- if (s.value) parts.push(`= "${s.value}"`);
2054
- return parts.join(" ");
2055
- }).join("\n");
2056
- const systemPrompt = `You write professional demo video captions in ${langName}.
2057
- Given UI steps, return a JSON array of captions \u2014 one per step, in order.
2058
-
2059
- RULES:
2060
- - Return ONLY a valid JSON array of strings, no explanation
2061
- - Each caption: 1-2 short conversational sentences, present tense ("We click...", "Here we type...")
2062
- - Describe what the user is doing and why it matters in the context of the app
2067
+ // src/help.ts
2068
+ var import_chalk = __toESM(require("chalk"));
2069
+ init_lib();
2063
2070
 
2064
- OUTPUT: ["Caption for step 1.", "Caption for step 2.", ...]`;
2065
- const contextLine = pageContext ? `
2066
- App context: ${pageContext}` : "";
2067
- const userMessage = `App: ${title} (${url})${contextLine}
2071
+ // src/logo.ts
2072
+ var import_figlet = __toESM(require("figlet"));
2073
+ var import_gradient_string = __toESM(require("gradient-string"));
2074
+ var DEFAULT_LOGO_CONFIG = {
2075
+ text: "QuickPeek",
2076
+ font: "Slant",
2077
+ horizontalLayout: "default",
2078
+ gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
2079
+ suffix: " \u{1F440}"
2080
+ };
2081
+ function renderLogo(config = {}) {
2082
+ const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
2083
+ const ascii = import_figlet.default.textSync(resolved.text, {
2084
+ font: resolved.font,
2085
+ horizontalLayout: resolved.horizontalLayout
2086
+ });
2087
+ const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
2088
+ return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
2089
+ }
2090
+
2091
+ // src/utils/config-loader.ts
2092
+ var import_node_fs4 = require("fs");
2093
+ var import_promises5 = require("fs/promises");
2094
+ var import_node_os2 = require("os");
2095
+ var import_node_path7 = require("path");
2096
+ init_lib();
2097
+ async function loadConfig(_url, langOverride) {
2098
+ const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
2099
+ if ((0, import_node_fs4.existsSync)(configPath)) {
2100
+ try {
2101
+ const content = await (0, import_promises5.readFile)(configPath, "utf-8");
2102
+ const userConfig = JSON.parse(content);
2103
+ const config = {
2104
+ lang: langOverride || userConfig.lang || "en",
2105
+ plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2106
+ video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2107
+ audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2108
+ music: userConfig.music,
2109
+ timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2110
+ output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2111
+ };
2112
+ return { config, created: false };
2113
+ } catch {
2114
+ }
2115
+ }
2116
+ const fullDefaultConfig = {
2117
+ lang: "en",
2118
+ plan: { maxSteps: 8 },
2119
+ video: {
2120
+ width: 1080,
2121
+ height: 1920,
2122
+ fps: 30,
2123
+ quality: 0.5,
2124
+ preset: "fast",
2125
+ cursor: true,
2126
+ highlight: false,
2127
+ fadeIn: 500,
2128
+ // ms
2129
+ fadeOut: 500,
2130
+ // ms
2131
+ contrast: 0.25
2132
+ },
2133
+ audio: {
2134
+ // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2135
+ // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2136
+ speed: 1
2137
+ },
2138
+ music: {
2139
+ path: "",
2140
+ volume: 0.3
2141
+ },
2142
+ output: { dir: "./demo" }
2143
+ };
2144
+ await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2145
+ return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2146
+ }
2147
+ var WORK_DIR = ".quickpeek";
2148
+ async function prepareWorkDir() {
2149
+ const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
2150
+ if ((0, import_node_fs4.existsSync)(workDir)) {
2151
+ await (0, import_promises5.rm)(workDir, { recursive: true });
2152
+ }
2153
+ await (0, import_promises5.mkdir)(workDir, { recursive: true });
2154
+ return workDir;
2155
+ }
2156
+ var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
2157
+ var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
2158
+ async function loadUserData() {
2159
+ try {
2160
+ if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
2161
+ return JSON.parse(await (0, import_promises5.readFile)(USER_DATA_FILE, "utf-8"));
2162
+ }
2163
+ } catch {
2164
+ }
2165
+ return {};
2166
+ }
2167
+ async function saveUserData(userData) {
2168
+ if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
2169
+ await (0, import_promises5.mkdir)(USER_DATA_DIR, { recursive: true });
2170
+ }
2171
+ const existing = await loadUserData();
2172
+ const merged = { ...existing, ...userData };
2173
+ await (0, import_promises5.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
2174
+ }
2175
+ var DEFAULT_CAPTIONS_CSS = `.caption {
2176
+ font-family: Arial, sans-serif;
2177
+ -webkit-text-stroke: 3px #000000;
2178
+ }
2179
+ .caption .regular {
2180
+ font-size: 56px;
2181
+ color: #FFFFFF;
2182
+ }
2183
+ .caption .highlight {
2184
+ font-size: 64px;
2185
+ color: #FFFF00;
2186
+ }
2187
+ `;
2188
+ function parseCaptionCSS(css) {
2189
+ const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
2190
+ const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
2191
+ const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
2192
+ const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
2193
+ const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
2194
+ if (fontMatch) style.font = fontMatch[1].trim();
2195
+ const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
2196
+ if (strokeMatch) {
2197
+ style.outlineWidth = parseInt(strokeMatch[1], 10);
2198
+ style.outline = rgbaToHex(strokeMatch[2]);
2199
+ }
2200
+ const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
2201
+ if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
2202
+ const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
2203
+ if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
2204
+ const parseWordStyle = (block, target) => {
2205
+ const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2206
+ if (colorMatch) target.color = colorMatch[1];
2207
+ const sizeMatch = block.match(/font-size:\s*(\d+)/);
2208
+ if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
2209
+ const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
2210
+ if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2211
+ };
2212
+ parseWordStyle(regularBlock, style.regular);
2213
+ parseWordStyle(highlightBlock, style.highlight);
2214
+ if (!regularBlock && !highlightBlock) {
2215
+ const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2216
+ if (colorMatch) style.regular.color = colorMatch[1];
2217
+ const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
2218
+ if (sizeMatch) {
2219
+ style.regular.size = parseInt(sizeMatch[1], 10);
2220
+ style.highlight.size = Math.round(style.regular.size * 1.15);
2221
+ }
2222
+ const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
2223
+ if (highlightMatch) style.highlight.color = highlightMatch[1];
2224
+ const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
2225
+ if (weightMatch) {
2226
+ const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2227
+ style.regular.bold = isBold;
2228
+ style.highlight.bold = isBold;
2229
+ }
2230
+ }
2231
+ return style;
2232
+ }
2233
+ async function loadCaptions() {
2234
+ const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
2235
+ if (!(0, import_node_fs4.existsSync)(captionsPath)) {
2236
+ await (0, import_promises5.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
2237
+ return DEFAULT_CAPTIONS;
2238
+ }
2239
+ try {
2240
+ const css = await (0, import_promises5.readFile)(captionsPath, "utf-8");
2241
+ return parseCaptionCSS(css);
2242
+ } catch {
2243
+ return DEFAULT_CAPTIONS;
2244
+ }
2245
+ }
2246
+ async function savePlan(plan, planPath) {
2247
+ const cleanPlan = {
2248
+ ...plan,
2249
+ steps: plan.steps.map(({ duration, ...step }) => step)
2250
+ };
2251
+ await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
2252
+ await (0, import_promises5.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
2253
+ }
2254
+
2255
+ // src/help.ts
2256
+ var cmd = import_chalk.default.cyan;
2257
+ var arg = import_chalk.default.green;
2258
+ var opt = import_chalk.default.magenta;
2259
+ var desc = import_chalk.default.white;
2260
+ var ex = import_chalk.default.dim;
2261
+ function pad(colored, visible, width) {
2262
+ return colored + " ".repeat(Math.max(0, width - visible.length));
2263
+ }
2264
+ function showLogo() {
2265
+ const logo = renderLogo();
2266
+ console.log(logo);
2267
+ console.log(import_chalk.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
2268
+ console.log(import_chalk.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
2269
+ }
2270
+ function formatUsageLines(status, tier) {
2271
+ const lines = [];
2272
+ lines.push(`Videos generated: ${status.usage_count}`);
2273
+ if (tier !== "pro" && status.trial) {
2274
+ const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
2275
+ const used = max_uses - uses_remaining;
2276
+ lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
2277
+ if (!is_expired && days_remaining > 0) {
2278
+ lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
2279
+ }
2280
+ }
2281
+ if (tier === "pro" && status.subscription) {
2282
+ const end = status.subscription.current_period_end;
2283
+ if (status.subscription.is_lifetime) {
2284
+ lines.push("Lifetime access");
2285
+ } else if (isCanceling(status)) {
2286
+ const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
2287
+ lines.push(`Cancels: ${date}`);
2288
+ } else if (end) {
2289
+ const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
2290
+ lines.push(`Renews: ${date}`);
2291
+ }
2292
+ }
2293
+ return lines;
2294
+ }
2295
+ async function showAccount() {
2296
+ const userData = await loadUserData();
2297
+ if (!userData.email) {
2298
+ console.log(import_chalk.default.dim(" Not signed in. Run ") + cmd("qp login") + import_chalk.default.dim(" to sign in."));
2299
+ console.log();
2300
+ return;
2301
+ }
2302
+ const email = userData.email;
2303
+ const tier = userData.tier === "pro" ? "pro" : "free";
2304
+ const tierLabel = tier === "pro" ? import_chalk.default.green("Pro") : "Free";
2305
+ console.log(` ${tierLabel} plan ${import_chalk.default.dim("\xB7")} ${import_chalk.default.dim(email)}`);
2306
+ try {
2307
+ const status = await getStatusCached(email);
2308
+ for (const line of formatUsageLines(status, tier)) {
2309
+ console.log(import_chalk.default.dim(` ${line}`));
2310
+ }
2311
+ } catch {
2312
+ }
2313
+ console.log();
2314
+ }
2315
+ async function showHelp() {
2316
+ showLogo();
2317
+ await showAccount();
2318
+ const W = 28;
2319
+ const usage = [
2320
+ [`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
2321
+ [
2322
+ `${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`,
2323
+ `qp <url> --script <file>`,
2324
+ "Use your own narration script"
2325
+ ],
2326
+ [`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
2327
+ [`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
2328
+ [`${cmd("qp logout")}`, `qp logout`, "Sign out"],
2329
+ [`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
2330
+ [`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
2331
+ [`${cmd("qp init")}`, `qp init`, "Create config file"],
2332
+ [`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
2333
+ ];
2334
+ const options = [
2335
+ [`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
2336
+ [
2337
+ `${opt("-d")}, ${opt("--description")} ${arg('"..."')}`,
2338
+ `-d, --description "..."`,
2339
+ "Guide AI with an app description"
2340
+ ],
2341
+ [
2342
+ `${opt("-s")}, ${opt("--script")} ${arg("<file>")}`,
2343
+ `-s, --script <file>`,
2344
+ "Narration script (one caption per line)"
2345
+ ],
2346
+ [`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
2347
+ [`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
2348
+ [`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
2349
+ ];
2350
+ const examples = [
2351
+ `${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
2352
+ `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
2353
+ `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
2354
+ `${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
2355
+ ];
2356
+ console.log();
2357
+ console.log(import_chalk.default.yellow.bold("USAGE"));
2358
+ for (const [colored, plain, d] of usage) {
2359
+ console.log(` ${pad(colored, plain, W)}${desc(d)}`);
2360
+ }
2361
+ console.log();
2362
+ console.log(import_chalk.default.yellow.bold("OPTIONS"));
2363
+ for (const [colored, plain, d] of options) {
2364
+ console.log(` ${pad(colored, plain, W)}${desc(d)}`);
2365
+ }
2366
+ console.log();
2367
+ console.log(import_chalk.default.yellow.bold("EXAMPLES"));
2368
+ for (const e of examples) {
2369
+ console.log(` ${e}`);
2370
+ }
2371
+ console.log();
2372
+ }
2373
+
2374
+ // src/utils/ui.ts
2375
+ var import_node_path8 = require("path");
2376
+ var import_node_readline = require("readline");
2377
+ var import_chalk2 = __toESM(require("chalk"));
2378
+ var import_ora = __toESM(require("ora"));
2379
+ var INDENT = " ";
2380
+ var autoYes = false;
2381
+ function setAutoYes(value) {
2382
+ autoYes = value;
2383
+ }
2384
+ function task(text) {
2385
+ return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
2386
+ }
2387
+ function header(text) {
2388
+ console.log();
2389
+ console.log(import_chalk2.default.bold.cyan(`\u25A0 ${text}`));
2390
+ }
2391
+ function info(text) {
2392
+ console.log(`${INDENT}${import_chalk2.default.dim("\u2502")} ${import_chalk2.default.dim(text)}`);
2393
+ }
2394
+ function confirm(prompt2) {
2395
+ if (autoYes) return Promise.resolve(true);
2396
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
2397
+ return new Promise((res) => {
2398
+ rl.question(import_chalk2.default.yellow(`${prompt2} [Y/n] `), (answer) => {
2399
+ rl.close();
2400
+ res(answer.toLowerCase() !== "n");
2401
+ });
2402
+ });
2403
+ }
2404
+ function promptInput(prompt2) {
2405
+ if (autoYes) return Promise.resolve("");
2406
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
2407
+ return new Promise((res) => {
2408
+ rl.question(import_chalk2.default.cyan(`${INDENT}${prompt2}`), (answer) => {
2409
+ rl.close();
2410
+ res(answer.trim());
2411
+ });
2412
+ });
2413
+ }
2414
+ var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2415
+ function formatClickablePath(filePath) {
2416
+ const absolutePath = (0, import_node_path8.resolve)(filePath);
2417
+ if (_isWSL) {
2418
+ return absolutePath;
2419
+ }
2420
+ let formatted = absolutePath;
2421
+ formatted = formatted.replace(/\//g, "\\");
2422
+ if (formatted.includes(" ") && !formatted.startsWith('"')) {
2423
+ formatted = `"${formatted}"`;
2424
+ }
2425
+ return formatted;
2426
+ }
2427
+ function formatDuration(ms) {
2428
+ const mins = Math.floor(ms / 6e4);
2429
+ const secs = Math.floor(ms % 6e4 / 1e3);
2430
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
2431
+ }
2432
+
2433
+ // src/commands/auth-cmd.ts
2434
+ function waitForEnter(prompt2) {
2435
+ return new Promise((resolve9) => {
2436
+ const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
2437
+ rl.question(prompt2, () => {
2438
+ rl.close();
2439
+ resolve9();
2440
+ });
2441
+ });
2442
+ }
2443
+ async function authCommand(url) {
2444
+ console.log();
2445
+ showLogo();
2446
+ if (!authStatePath(url)) {
2447
+ console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
2448
+ return;
2449
+ }
2450
+ console.log(import_chalk3.default.white(" A browser will open. Sign in to the site, then come back here."));
2451
+ console.log();
2452
+ const browser = await import_playwright2.chromium.launch({ headless: false });
2453
+ const context = await browser.newContext();
2454
+ const page = await context.newPage();
2455
+ try {
2456
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
2457
+ } catch {
2458
+ }
2459
+ await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
2460
+ try {
2461
+ const state = await context.storageState();
2462
+ const path = await saveAuthState(url, state);
2463
+ console.log();
2464
+ console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
2465
+ console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
2466
+ console.log(import_chalk3.default.dim(" Crawls and recordings of this site now start signed in."));
2467
+ console.log(import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan(`qp auth clear ${new URL(url).host}`) + import_chalk3.default.dim(" to forget it."));
2468
+ } finally {
2469
+ await browser.close().catch(() => {
2470
+ });
2471
+ }
2472
+ console.log();
2473
+ }
2474
+ async function authClearCommand(url) {
2475
+ console.log();
2476
+ const removed = await clearAuthState(url);
2477
+ if (removed.length === 0) {
2478
+ console.log(import_chalk3.default.dim(url ? ` No saved session for ${url}` : " No saved sessions."));
2479
+ } else {
2480
+ for (const host of removed) {
2481
+ console.log(import_chalk3.default.green(` \u2713 Forgot session for ${host}`));
2482
+ }
2483
+ }
2484
+ console.log();
2485
+ }
2486
+ async function authListCommand() {
2487
+ console.log();
2488
+ const hosts = await listAuthStates();
2489
+ if (hosts.length === 0) {
2490
+ console.log(import_chalk3.default.dim(" No saved sessions. Run ") + import_chalk3.default.cyan("qp auth <url>") + import_chalk3.default.dim(" to add one."));
2491
+ } else {
2492
+ console.log(import_chalk3.default.white(" Saved sessions:"));
2493
+ for (const host of hosts) {
2494
+ console.log(` ${import_chalk3.default.green("\u25CF")} ${host}`);
2495
+ }
2496
+ }
2497
+ console.log();
2498
+ }
2499
+
2500
+ // src/commands/capture-cmd.ts
2501
+ var import_node_path20 = require("path");
2502
+ var import_chalk11 = __toESM(require("chalk"));
2503
+
2504
+ // src/ai/narrate.ts
2505
+ init_lib();
2506
+
2507
+ // src/ai/client.ts
2508
+ var import_node_readline3 = require("readline");
2509
+ var import_chalk5 = __toESM(require("chalk"));
2510
+ init_lib();
2511
+
2512
+ // src/utils/browser-auth.ts
2513
+ var import_node_http = require("http");
2514
+ var import_node_url = require("url");
2515
+ var import_chalk4 = __toESM(require("chalk"));
2516
+ init_SparkStripe_feature_gating();
2517
+ var TIMEOUT_MS = 12e4;
2518
+ async function openBrowser(url) {
2519
+ const { exec: exec2 } = await import("child_process");
2520
+ const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
2521
+ const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
2522
+ return new Promise((resolve9) => {
2523
+ exec2(command, () => resolve9());
2524
+ });
2525
+ }
2526
+ async function browserLogin() {
2527
+ return new Promise((resolve9) => {
2528
+ let server;
2529
+ let timer;
2530
+ const cleanup = (email) => {
2531
+ clearTimeout(timer);
2532
+ server?.close();
2533
+ resolve9(email);
2534
+ };
2535
+ server = (0, import_node_http.createServer)((req, res) => {
2536
+ const url = new import_node_url.URL(req.url || "/", `http://localhost`);
2537
+ const path = url.pathname;
2538
+ if (path === "/callback" || path === "/") {
2539
+ const email = url.searchParams.get("email");
2540
+ res.writeHead(200, { "Content-Type": "text/html" });
2541
+ res.end(`
2542
+ <!DOCTYPE html>
2543
+ <html>
2544
+ <head><title>QuickPeek</title><style>
2545
+ body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0a0a0a; color: #fff; }
2546
+ .card { text-align: center; padding: 2rem; }
2547
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
2548
+ p { color: #888; }
2549
+ </style></head>
2550
+ <body><div class="card">
2551
+ <h1>${email ? "Logged in!" : "Return to your terminal"}</h1>
2552
+ <p>${email ? `Signed in as ${email}` : "Complete setup in your terminal."}</p>
2553
+ <p style="margin-top: 1rem; font-size: 0.875rem;">You can close this tab.</p>
2554
+ </div></body>
2555
+ </html>
2556
+ `);
2557
+ if (email) {
2558
+ cleanup(email);
2559
+ }
2560
+ } else {
2561
+ res.writeHead(404);
2562
+ res.end();
2563
+ }
2564
+ });
2565
+ server.listen(0, "127.0.0.1", () => {
2566
+ const addr = server.address();
2567
+ if (!addr || typeof addr === "string") {
2568
+ cleanup(null);
2569
+ return;
2570
+ }
2571
+ const port = addr.port;
2572
+ const returnUrl = `http://localhost:${port}/callback`;
2573
+ const url = pricingUrl({ returnUrl });
2574
+ console.log();
2575
+ console.log(import_chalk4.default.cyan(" Opening browser to sign in..."));
2576
+ console.log(import_chalk4.default.dim(` ${url}`));
2577
+ console.log();
2578
+ console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
2579
+ openBrowser(url);
2580
+ });
2581
+ timer = setTimeout(() => {
2582
+ console.log();
2583
+ console.log(import_chalk4.default.yellow(" Sign-in timed out."));
2584
+ cleanup(null);
2585
+ }, TIMEOUT_MS);
2586
+ });
2587
+ }
2588
+
2589
+ // src/ai/client.ts
2590
+ var lastRateLimitInfo = null;
2591
+ function getLastRateLimitInfo() {
2592
+ return lastRateLimitInfo;
2593
+ }
2594
+ function prompt(question) {
2595
+ const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
2596
+ return new Promise((resolve9) => {
2597
+ rl.question(question, (answer) => {
2598
+ rl.close();
2599
+ resolve9(answer.trim());
2600
+ });
2601
+ });
2602
+ }
2603
+ async function setupUserEmail() {
2604
+ console.log();
2605
+ console.log(import_chalk5.default.yellow(" \u2709 Account Setup"));
2606
+ console.log(import_chalk5.default.dim(" \u2500".repeat(20)));
2607
+ console.log();
2608
+ console.log(import_chalk5.default.white(" Sign in with your Google account to track usage"));
2609
+ console.log(import_chalk5.default.white(" and unlock Pro features."));
2610
+ const browserEmail = await browserLogin();
2611
+ if (browserEmail) {
2612
+ console.log();
2613
+ console.log(import_chalk5.default.green(` \u2713 Signed in as ${browserEmail}`));
2614
+ console.log();
2615
+ return browserEmail;
2616
+ }
2617
+ console.log();
2618
+ console.log(import_chalk5.default.dim(" Or enter your email manually:"));
2619
+ const email = await prompt(import_chalk5.default.white(" Email: "));
2620
+ if (!email) {
2621
+ console.log();
2622
+ console.log(import_chalk5.default.red(" No email provided. Exiting."));
2623
+ process.exit(1);
2624
+ }
2625
+ if (!email.includes("@") || !email.includes(".")) {
2626
+ console.log();
2627
+ console.log(import_chalk5.default.yellow(" \u26A0 That doesn't look like a valid email address."));
2628
+ const proceed = await prompt(import_chalk5.default.white(" Continue anyway? ") + import_chalk5.default.dim("[y/N] "));
2629
+ if (proceed.toLowerCase() !== "y") {
2630
+ process.exit(1);
2631
+ }
2632
+ }
2633
+ console.log();
2634
+ console.log(import_chalk5.default.green(" \u2713 Email registered"));
2635
+ console.log();
2636
+ return email;
2637
+ }
2638
+ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
2639
+ const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier);
2640
+ if (!result.success) {
2641
+ await logError2("AI API", new Error(result.error));
2642
+ if (result.isRateLimit) {
2643
+ console.log();
2644
+ console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
2645
+ console.log();
2646
+ console.log(import_chalk5.default.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
2647
+ console.log(import_chalk5.default.dim(" Options:"));
2648
+ if (result.retryAfterSeconds) {
2649
+ const minutes = Math.ceil(result.retryAfterSeconds / 60);
2650
+ console.log(import_chalk5.default.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
2651
+ } else {
2652
+ console.log(import_chalk5.default.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
2653
+ }
2654
+ console.log(import_chalk5.default.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
2655
+ console.log(import_chalk5.default.dim(" \u2022 Upgrade to a higher tier for more requests"));
2656
+ console.log();
2657
+ process.exit(1);
2658
+ }
2659
+ if (result.isAuthError) {
2660
+ console.log();
2661
+ console.log(import_chalk5.default.red(" \u26A0 Authentication Error"));
2662
+ console.log();
2663
+ console.log(import_chalk5.default.dim(` ${result.error}`));
2664
+ console.log();
2665
+ process.exit(1);
2666
+ }
2667
+ console.log();
2668
+ console.log(import_chalk5.default.red(" \u26A0 AI Error"));
2669
+ console.log();
2670
+ const firstLine = result.error.split("\n")[0] ?? result.error;
2671
+ console.log(import_chalk5.default.dim(` ${firstLine.slice(0, 80)}`));
2672
+ console.log();
2673
+ process.exit(1);
2674
+ }
2675
+ lastRateLimitInfo = result.limits;
2676
+ return result.content;
2677
+ }
2678
+
2679
+ // src/ai/narrate.ts
2680
+ async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
2681
+ const langName = getLanguageName(lang);
2682
+ const stepList = steps.map((s, i) => {
2683
+ const parts = [`${i + 1}. ${s.action}`];
2684
+ if (s.target) parts.push(`on "${s.target}"`);
2685
+ if (s.value) parts.push(`= "${s.value}"`);
2686
+ return parts.join(" ");
2687
+ }).join("\n");
2688
+ const systemPrompt = `You write professional demo video captions in ${langName}.
2689
+ Given UI steps, return a JSON array of captions \u2014 one per step, in order.
2690
+
2691
+ RULES:
2692
+ - Return ONLY a valid JSON array of strings, no explanation
2693
+ - Each caption: 1-2 short conversational sentences, present tense ("We click...", "Here we type...")
2694
+ - Describe what the user is doing and why it matters in the context of the app
2695
+
2696
+ OUTPUT: ["Caption for step 1.", "Caption for step 2.", ...]`;
2697
+ const contextLine = pageContext ? `
2698
+ App context: ${pageContext}` : "";
2699
+ const userMessage = `App: ${title} (${url})${contextLine}
2068
2700
 
2069
2701
  Steps to caption:
2070
2702
  ${stepList}
@@ -2162,11 +2794,11 @@ function eventsToSteps(events, url) {
2162
2794
  let prevClickSelector;
2163
2795
  let prevClickTime = 0;
2164
2796
  let lastNavigatedUrl;
2165
- const normalizedOriginUrl = normalizeUrl(url);
2797
+ const normalizedOriginUrl = normalizeUrl2(url);
2166
2798
  for (const event of events) {
2167
2799
  if (event.type === "navigate") {
2168
2800
  if (!event.url) continue;
2169
- const normalized = normalizeUrl(event.url);
2801
+ const normalized = normalizeUrl2(event.url);
2170
2802
  if (normalized === normalizedOriginUrl) continue;
2171
2803
  if (lastNavigatedUrl !== void 0 && normalized === lastNavigatedUrl) continue;
2172
2804
  lastNavigatedUrl = normalized;
@@ -2216,7 +2848,7 @@ function eventsToSteps(events, url) {
2216
2848
  }
2217
2849
  return steps;
2218
2850
  }
2219
- function normalizeUrl(url) {
2851
+ function normalizeUrl2(url) {
2220
2852
  try {
2221
2853
  const u = new URL(url);
2222
2854
  return u.origin + u.pathname.replace(/\/$/, "");
@@ -2227,15 +2859,15 @@ function normalizeUrl(url) {
2227
2859
 
2228
2860
  // src/capture.ts
2229
2861
  var import_node_child_process4 = require("child_process");
2230
- var import_node_http2 = require("http");
2231
- var import_node_readline2 = require("readline");
2232
- var import_playwright3 = require("playwright");
2862
+ var import_node_http3 = require("http");
2863
+ var import_node_readline4 = require("readline");
2864
+ var import_playwright4 = require("playwright");
2233
2865
 
2234
2866
  // src/browser/wsl.ts
2235
2867
  var import_node_child_process3 = require("child_process");
2236
- var import_node_fs3 = require("fs");
2237
- var import_node_http = require("http");
2238
- var import_playwright2 = require("playwright");
2868
+ var import_node_fs5 = require("fs");
2869
+ var import_node_http2 = require("http");
2870
+ var import_playwright3 = require("playwright");
2239
2871
  function isWSL() {
2240
2872
  if (process.platform !== "linux") return false;
2241
2873
  try {
@@ -2246,7 +2878,7 @@ function isWSL() {
2246
2878
  }
2247
2879
  }
2248
2880
  function tryEnableWSLInterop() {
2249
- return (0, import_node_fs3.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2881
+ return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
2250
2882
  }
2251
2883
  function wslToWindowsPath(wslPath) {
2252
2884
  if (!wslPath) return wslPath;
@@ -2255,7 +2887,7 @@ function wslToWindowsPath(wslPath) {
2255
2887
  return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
2256
2888
  }
2257
2889
  function findExe(whereExeQuery, ...hardcoded) {
2258
- for (const p of hardcoded) if ((0, import_node_fs3.existsSync)(p)) return p;
2890
+ for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
2259
2891
  try {
2260
2892
  const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
2261
2893
  if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
@@ -2278,7 +2910,7 @@ function getWindowsHostIP() {
2278
2910
  } catch {
2279
2911
  }
2280
2912
  try {
2281
- const m = (0, import_node_fs3.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2913
+ const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
2282
2914
  if (m?.[1]) return m[1];
2283
2915
  } catch {
2284
2916
  }
@@ -2312,7 +2944,7 @@ function waitForChrome(host, port) {
2312
2944
  reject(new Error("Chrome debug port did not open within 20 s"));
2313
2945
  return;
2314
2946
  }
2315
- const req = (0, import_node_http.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
2947
+ const req = (0, import_node_http2.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
2316
2948
  res.resume();
2317
2949
  resolve9();
2318
2950
  });
@@ -2354,7 +2986,7 @@ async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
2354
2986
  const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
2355
2987
  try {
2356
2988
  await waitForChrome(windowsHost, proxyPort);
2357
- const browser = await import_playwright2.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2989
+ const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
2358
2990
  return {
2359
2991
  browser,
2360
2992
  cleanup: async () => {
@@ -2483,9 +3115,9 @@ var EVENT_TRACKER_SCRIPT = `
2483
3115
  }, { passive: true, capture: true });
2484
3116
  }());
2485
3117
  `;
2486
- function waitForEnter() {
3118
+ function waitForEnter2() {
2487
3119
  return new Promise((resolve9) => {
2488
- const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
3120
+ const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
2489
3121
  process.stdout.write(" Press Enter when done...\n");
2490
3122
  rl.once("line", () => {
2491
3123
  rl.close();
@@ -2502,7 +3134,7 @@ async function launchCaptureChrome() {
2502
3134
  const windowsHost = getWindowsHostIP();
2503
3135
  const probe = await startRelay(nodePath, windowsHost, debugPort);
2504
3136
  const alreadyUp = await new Promise((resolve9) => {
2505
- const req = (0, import_node_http2.request)({ host: windowsHost, port: probe.port, path: "/json/version", method: "GET" }, (res) => {
3137
+ const req = (0, import_node_http3.request)({ host: windowsHost, port: probe.port, path: "/json/version", method: "GET" }, (res) => {
2506
3138
  res.resume();
2507
3139
  resolve9(true);
2508
3140
  });
@@ -2514,7 +3146,7 @@ async function launchCaptureChrome() {
2514
3146
  req.end();
2515
3147
  });
2516
3148
  if (alreadyUp) {
2517
- const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
3149
+ const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${probe.port}`, { timeout: 5e3 });
2518
3150
  return { browser, relay: probe.proc };
2519
3151
  }
2520
3152
  probe.proc.kill();
@@ -2534,7 +3166,7 @@ async function launchCaptureChrome() {
2534
3166
  const relay = await startRelay(nodePath, windowsHost, debugPort);
2535
3167
  try {
2536
3168
  await waitForChrome(windowsHost, relay.port);
2537
- const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
3169
+ const browser = await import_playwright4.chromium.connectOverCDP(`http://${windowsHost}:${relay.port}`, { timeout: 5e3 });
2538
3170
  return { browser, relay: relay.proc };
2539
3171
  } catch (err) {
2540
3172
  relay.proc.kill();
@@ -2549,7 +3181,7 @@ async function captureUserSession(url, _workDir) {
2549
3181
  if (isWSL()) {
2550
3182
  ({ browser, relay } = await launchCaptureChrome());
2551
3183
  } else {
2552
- browser = await import_playwright3.chromium.launch({
3184
+ browser = await import_playwright4.chromium.launch({
2553
3185
  headless: false,
2554
3186
  args: ["--start-maximized", "--no-first-run"]
2555
3187
  });
@@ -2591,7 +3223,7 @@ async function captureUserSession(url, _workDir) {
2591
3223
  return [title, parts.join(" | ")];
2592
3224
  }).catch(() => ["", ""]);
2593
3225
  startTime = Date.now();
2594
- await waitForEnter();
3226
+ await waitForEnter2();
2595
3227
  const durationMs = Date.now() - startTime;
2596
3228
  if (relay) {
2597
3229
  try {
@@ -2606,195 +3238,31 @@ async function captureUserSession(url, _workDir) {
2606
3238
  return { events, durationMs, pageTitle, pageContext };
2607
3239
  }
2608
3240
 
2609
- // src/utils/config-loader.ts
2610
- var import_node_fs4 = require("fs");
2611
- var import_promises4 = require("fs/promises");
2612
- var import_node_os = require("os");
2613
- var import_node_path6 = require("path");
2614
- init_lib();
2615
- async function loadConfig(_url, langOverride) {
2616
- const configPath = (0, import_node_path6.join)(process.cwd(), CONFIG_FILE);
2617
- if ((0, import_node_fs4.existsSync)(configPath)) {
2618
- try {
2619
- const content = await (0, import_promises4.readFile)(configPath, "utf-8");
2620
- const userConfig = JSON.parse(content);
2621
- const config = {
2622
- lang: langOverride || userConfig.lang || "en",
2623
- plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
2624
- video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
2625
- audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
2626
- music: userConfig.music,
2627
- timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
2628
- output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
2629
- };
2630
- return { config, created: false };
2631
- } catch {
2632
- }
2633
- }
2634
- const fullDefaultConfig = {
2635
- lang: "en",
2636
- plan: { maxSteps: 8 },
2637
- video: {
2638
- width: 1080,
2639
- height: 1920,
2640
- fps: 30,
2641
- quality: 0.5,
2642
- preset: "fast",
2643
- cursor: true,
2644
- highlight: false,
2645
- fadeIn: 500,
2646
- // ms
2647
- fadeOut: 500,
2648
- // ms
2649
- contrast: 0.25
2650
- },
2651
- audio: {
2652
- // Voice is derived from lang setting via LANGUAGE_VOICES mapping
2653
- // To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
2654
- speed: 1
2655
- },
2656
- music: {
2657
- path: "",
2658
- volume: 0.3
2659
- },
2660
- output: { dir: "./demo" }
2661
- };
2662
- await (0, import_promises4.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
2663
- return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
2664
- }
2665
- var WORK_DIR = ".quickpeek";
2666
- async function prepareWorkDir() {
2667
- const workDir = (0, import_node_path6.join)(process.cwd(), WORK_DIR);
2668
- if ((0, import_node_fs4.existsSync)(workDir)) {
2669
- await (0, import_promises4.rm)(workDir, { recursive: true });
2670
- }
2671
- await (0, import_promises4.mkdir)(workDir, { recursive: true });
2672
- return workDir;
2673
- }
2674
- var USER_DATA_DIR = (0, import_node_path6.join)((0, import_node_os.homedir)(), ".quickpeek");
2675
- var USER_DATA_FILE = (0, import_node_path6.join)(USER_DATA_DIR, "user.json");
2676
- async function loadUserData() {
2677
- try {
2678
- if ((0, import_node_fs4.existsSync)(USER_DATA_FILE)) {
2679
- return JSON.parse(await (0, import_promises4.readFile)(USER_DATA_FILE, "utf-8"));
2680
- }
2681
- } catch {
2682
- }
2683
- return {};
2684
- }
2685
- async function saveUserData(userData) {
2686
- if (!(0, import_node_fs4.existsSync)(USER_DATA_DIR)) {
2687
- await (0, import_promises4.mkdir)(USER_DATA_DIR, { recursive: true });
2688
- }
2689
- const existing = await loadUserData();
2690
- const merged = { ...existing, ...userData };
2691
- await (0, import_promises4.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
2692
- }
2693
- var DEFAULT_CAPTIONS_CSS = `.caption {
2694
- font-family: Arial, sans-serif;
2695
- -webkit-text-stroke: 3px #000000;
2696
- }
2697
- .caption .regular {
2698
- font-size: 56px;
2699
- color: #FFFFFF;
2700
- }
2701
- .caption .highlight {
2702
- font-size: 64px;
2703
- color: #FFFF00;
2704
- }
2705
- `;
2706
- function parseCaptionCSS(css) {
2707
- const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
2708
- const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
2709
- const regularBlock = css.match(/\.caption\s+\.regular\s*\{([^}]+)\}/)?.[1] || "";
2710
- const highlightBlock = css.match(/\.caption\s+\.highlight\s*\{([^}]+)\}/)?.[1] || "";
2711
- const fontMatch = captionBlock.match(/font-family:\s*"?([^";,]+)/);
2712
- if (fontMatch) style.font = fontMatch[1].trim();
2713
- const strokeMatch = captionBlock.match(/-webkit-text-stroke:\s*(\d+)px\s*(rgba?\([^)]+\)|#[A-Fa-f0-9]{6})/);
2714
- if (strokeMatch) {
2715
- style.outlineWidth = parseInt(strokeMatch[1], 10);
2716
- style.outline = rgbaToHex(strokeMatch[2]);
2717
- }
2718
- const shadowMatch = captionBlock.match(/text-shadow:\s*(\d+)px/);
2719
- if (shadowMatch) style.shadow = parseInt(shadowMatch[1], 10);
2720
- const marginMatch = captionBlock.match(/margin-bottom:\s*(\d+)/);
2721
- if (marginMatch) style.marginBottom = parseInt(marginMatch[1], 10);
2722
- const parseWordStyle = (block, target) => {
2723
- const colorMatch = block.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2724
- if (colorMatch) target.color = colorMatch[1];
2725
- const sizeMatch = block.match(/font-size:\s*(\d+)/);
2726
- if (sizeMatch) target.size = parseInt(sizeMatch[1], 10);
2727
- const weightMatch = block.match(/font-weight:\s*(bold|normal|\d+)/);
2728
- if (weightMatch) target.bold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2729
- };
2730
- parseWordStyle(regularBlock, style.regular);
2731
- parseWordStyle(highlightBlock, style.highlight);
2732
- if (!regularBlock && !highlightBlock) {
2733
- const colorMatch = captionBlock.match(/[^-]color:\s*(#[A-Fa-f0-9]{6})/);
2734
- if (colorMatch) style.regular.color = colorMatch[1];
2735
- const sizeMatch = captionBlock.match(/font-size:\s*(\d+)/);
2736
- if (sizeMatch) {
2737
- style.regular.size = parseInt(sizeMatch[1], 10);
2738
- style.highlight.size = Math.round(style.regular.size * 1.15);
2739
- }
2740
- const highlightMatch = captionBlock.match(/-qp-highlight:\s*(#[A-Fa-f0-9]{6})/);
2741
- if (highlightMatch) style.highlight.color = highlightMatch[1];
2742
- const weightMatch = captionBlock.match(/font-weight:\s*(bold|normal|\d+)/);
2743
- if (weightMatch) {
2744
- const isBold = weightMatch[1] === "bold" || parseInt(weightMatch[1], 10) >= 700;
2745
- style.regular.bold = isBold;
2746
- style.highlight.bold = isBold;
2747
- }
2748
- }
2749
- return style;
2750
- }
2751
- async function loadCaptions() {
2752
- const captionsPath = (0, import_node_path6.join)(process.cwd(), "captions.css");
2753
- if (!(0, import_node_fs4.existsSync)(captionsPath)) {
2754
- await (0, import_promises4.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
2755
- return DEFAULT_CAPTIONS;
2756
- }
2757
- try {
2758
- const css = await (0, import_promises4.readFile)(captionsPath, "utf-8");
2759
- return parseCaptionCSS(css);
2760
- } catch {
2761
- return DEFAULT_CAPTIONS;
2762
- }
2763
- }
2764
- async function savePlan(plan, planPath) {
2765
- const cleanPlan = {
2766
- ...plan,
2767
- steps: plan.steps.map(({ duration, ...step }) => step)
2768
- };
2769
- await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(planPath), { recursive: true });
2770
- await (0, import_promises4.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
2771
- }
2772
-
2773
3241
  // src/utils/logging.ts
2774
- var import_promises5 = require("fs/promises");
2775
- var import_node_path7 = require("path");
3242
+ var import_promises6 = require("fs/promises");
3243
+ var import_node_path9 = require("path");
2776
3244
  var LOG_FILE_NAME = "quickpeek.log";
2777
3245
  var logFileInitialized = false;
2778
3246
  var logFilePath = LOG_FILE_NAME;
2779
3247
  async function writeLogEntry(entry) {
2780
3248
  try {
2781
3249
  if (!logFileInitialized) {
2782
- await (0, import_promises5.writeFile)(logFilePath, entry);
3250
+ await (0, import_promises6.writeFile)(logFilePath, entry);
2783
3251
  logFileInitialized = true;
2784
3252
  } else {
2785
- await (0, import_promises5.appendFile)(logFilePath, entry);
3253
+ await (0, import_promises6.appendFile)(logFilePath, entry);
2786
3254
  }
2787
3255
  } catch {
2788
3256
  }
2789
3257
  }
2790
3258
  async function setLogFilePath(outputDir) {
2791
- logFilePath = (0, import_node_path7.join)(outputDir, LOG_FILE_NAME);
3259
+ logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
2792
3260
  const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
2793
3261
  ${"=".repeat(60)}
2794
3262
 
2795
3263
  `;
2796
3264
  try {
2797
- await (0, import_promises5.writeFile)(logFilePath, header2);
3265
+ await (0, import_promises6.writeFile)(logFilePath, header2);
2798
3266
  logFileInitialized = true;
2799
3267
  } catch {
2800
3268
  }
@@ -2822,101 +3290,42 @@ ${errorMsg}
2822
3290
 
2823
3291
  `);
2824
3292
  }
2825
- async function logStep(stepNum, action, details) {
2826
- const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
2827
- await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
2828
- `);
2829
- }
2830
-
2831
- // src/utils/ui.ts
2832
- var import_node_path8 = require("path");
2833
- var import_node_readline3 = require("readline");
2834
- var import_chalk2 = __toESM(require("chalk"));
2835
- var import_ora = __toESM(require("ora"));
2836
- var INDENT = " ";
2837
- var autoYes = false;
2838
- function setAutoYes(value) {
2839
- autoYes = value;
2840
- }
2841
- function task(text) {
2842
- return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
2843
- }
2844
- function header(text) {
2845
- console.log();
2846
- console.log(import_chalk2.default.bold.cyan(`\u25A0 ${text}`));
2847
- }
2848
- function info(text) {
2849
- console.log(`${INDENT}${import_chalk2.default.dim("\u2502")} ${import_chalk2.default.dim(text)}`);
2850
- }
2851
- function confirm(prompt2) {
2852
- if (autoYes) return Promise.resolve(true);
2853
- const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
2854
- return new Promise((res) => {
2855
- rl.question(import_chalk2.default.yellow(`${prompt2} [Y/n] `), (answer) => {
2856
- rl.close();
2857
- res(answer.toLowerCase() !== "n");
2858
- });
2859
- });
2860
- }
2861
- function promptInput(prompt2) {
2862
- if (autoYes) return Promise.resolve("");
2863
- const rl = (0, import_node_readline3.createInterface)({ input: process.stdin, output: process.stdout });
2864
- return new Promise((res) => {
2865
- rl.question(import_chalk2.default.cyan(`${INDENT}${prompt2}`), (answer) => {
2866
- rl.close();
2867
- res(answer.trim());
2868
- });
2869
- });
2870
- }
2871
- var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
2872
- function formatClickablePath(filePath) {
2873
- const absolutePath = (0, import_node_path8.resolve)(filePath);
2874
- if (_isWSL) {
2875
- return absolutePath;
2876
- }
2877
- let formatted = absolutePath;
2878
- formatted = formatted.replace(/\//g, "\\");
2879
- if (formatted.includes(" ") && !formatted.startsWith('"')) {
2880
- formatted = `"${formatted}"`;
2881
- }
2882
- return formatted;
2883
- }
2884
- function formatDuration(ms) {
2885
- const mins = Math.floor(ms / 6e4);
2886
- const secs = Math.floor(ms % 6e4 / 1e3);
2887
- return `${mins}:${secs.toString().padStart(2, "0")}`;
2888
- }
3293
+ async function logStep(stepNum, action, details) {
3294
+ const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
3295
+ await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
3296
+ `);
3297
+ }
2889
3298
 
2890
3299
  // src/commands/phases/video-phase.ts
2891
- var import_node_fs11 = require("fs");
2892
- var import_promises12 = require("fs/promises");
2893
- var import_node_path17 = require("path");
3300
+ var import_node_fs12 = require("fs");
3301
+ var import_promises13 = require("fs/promises");
3302
+ var import_node_path18 = require("path");
2894
3303
  init_lib();
2895
3304
 
2896
3305
  // src/record.ts
2897
- var import_node_fs9 = require("fs");
2898
- var import_promises9 = require("fs/promises");
2899
- var import_node_path14 = require("path");
2900
- var import_chalk3 = __toESM(require("chalk"));
3306
+ var import_node_fs10 = require("fs");
3307
+ var import_promises10 = require("fs/promises");
3308
+ var import_node_path15 = require("path");
3309
+ var import_chalk6 = __toESM(require("chalk"));
2901
3310
 
2902
3311
  // src/browser/actions.ts
2903
- var import_node_fs6 = require("fs");
2904
- var import_promises6 = require("fs/promises");
2905
- var import_node_path10 = require("path");
3312
+ var import_node_fs7 = require("fs");
3313
+ var import_promises7 = require("fs/promises");
3314
+ var import_node_path11 = require("path");
2906
3315
 
2907
3316
  // src/browser/effects.ts
2908
- var import_node_fs5 = require("fs");
2909
- var import_node_path9 = require("path");
3317
+ var import_node_fs6 = require("fs");
3318
+ var import_node_path10 = require("path");
2910
3319
  init_lib();
2911
3320
  function loadHighlightCSS() {
2912
3321
  const candidates = [
2913
- (0, import_node_path9.join)(__dirname, "highlight.css"),
3322
+ (0, import_node_path10.join)(__dirname, "highlight.css"),
2914
3323
  // dist/ build
2915
- (0, import_node_path9.join)(__dirname, "..", "highlight.css")
3324
+ (0, import_node_path10.join)(__dirname, "..", "highlight.css")
2916
3325
  // src/browser/ → src/
2917
3326
  ];
2918
3327
  for (const p of candidates) {
2919
- if ((0, import_node_fs5.existsSync)(p)) return (0, import_node_fs5.readFileSync)(p, "utf-8");
3328
+ if ((0, import_node_fs6.existsSync)(p)) return (0, import_node_fs6.readFileSync)(p, "utf-8");
2920
3329
  }
2921
3330
  return "";
2922
3331
  }
@@ -3257,8 +3666,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3257
3666
  const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
3258
3667
  if (!isPlaceholder) {
3259
3668
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
3260
- const filePath = (0, import_node_path10.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path10.resolve)(baseDir, normalizedPath);
3261
- if ((0, import_node_fs6.existsSync)(filePath)) {
3669
+ const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
3670
+ if ((0, import_node_fs7.existsSync)(filePath)) {
3262
3671
  await fileInput.setInputFiles(filePath);
3263
3672
  await page.waitForTimeout(2e3);
3264
3673
  break;
@@ -3374,8 +3783,8 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3374
3783
  case "wait":
3375
3784
  break;
3376
3785
  case "image": {
3377
- const imagePath = step.value ? (0, import_node_path10.isAbsolute)(step.value) ? step.value : (0, import_node_path10.resolve)(baseDir, step.value) : null;
3378
- if (imagePath && (0, import_node_fs6.existsSync)(imagePath)) {
3786
+ const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
3787
+ if (imagePath && (0, import_node_fs7.existsSync)(imagePath)) {
3379
3788
  const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
3380
3789
  if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
3381
3790
  }
@@ -3405,7 +3814,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3405
3814
  await pendingDownload;
3406
3815
  pendingDownload = null;
3407
3816
  }
3408
- if (lastDownloadPath && (0, import_node_fs6.existsSync)(lastDownloadPath)) {
3817
+ if (lastDownloadPath && (0, import_node_fs7.existsSync)(lastDownloadPath)) {
3409
3818
  const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
3410
3819
  if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
3411
3820
  }
@@ -3416,7 +3825,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
3416
3825
  async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3417
3826
  const { width, height } = config.video;
3418
3827
  const ext = filePath.split(".").pop()?.toLowerCase() || "";
3419
- const fileBuffer = await (0, import_promises6.readFile)(filePath);
3828
+ const fileBuffer = await (0, import_promises7.readFile)(filePath);
3420
3829
  const base64 = fileBuffer.toString("base64");
3421
3830
  const mimeTypes = {
3422
3831
  png: "image/png",
@@ -3446,17 +3855,18 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
3446
3855
  }
3447
3856
 
3448
3857
  // src/browser/launch.ts
3449
- var import_promises7 = require("fs/promises");
3450
- var import_playwright4 = require("playwright");
3451
- async function launchBrowserContext(config, userDataDir, recordVideo) {
3858
+ var import_promises8 = require("fs/promises");
3859
+ var import_playwright5 = require("playwright");
3860
+ async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
3452
3861
  const extensionPath = config.browser?.extension;
3453
3862
  const viewport = { width: config.video.width, height: config.video.height };
3454
3863
  const videoOpts = recordVideo ? { recordVideo } : {};
3864
+ const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
3455
3865
  let context;
3456
3866
  let browser = null;
3457
3867
  let chromeSession = null;
3458
3868
  if (extensionPath) {
3459
- await (0, import_promises7.mkdir)(userDataDir, { recursive: true });
3869
+ await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
3460
3870
  const persistentArgs = [
3461
3871
  `--disable-extensions-except=${extensionPath}`,
3462
3872
  `--load-extension=${extensionPath}`,
@@ -3470,7 +3880,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
3470
3880
  const contexts = chromeSession.browser.contexts();
3471
3881
  context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
3472
3882
  } catch {
3473
- context = await import_playwright4.chromium.launchPersistentContext(userDataDir, {
3883
+ context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3474
3884
  headless: false,
3475
3885
  args: persistentArgs,
3476
3886
  viewport,
@@ -3479,7 +3889,7 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
3479
3889
  });
3480
3890
  }
3481
3891
  } else {
3482
- context = await import_playwright4.chromium.launchPersistentContext(userDataDir, {
3892
+ context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
3483
3893
  headless: false,
3484
3894
  args: persistentArgs,
3485
3895
  viewport,
@@ -3488,8 +3898,8 @@ async function launchBrowserContext(config, userDataDir, recordVideo) {
3488
3898
  });
3489
3899
  }
3490
3900
  } else {
3491
- browser = await import_playwright4.chromium.launch({ headless: true });
3492
- context = await browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
3901
+ browser = await import_playwright5.chromium.launch({ headless: true });
3902
+ context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
3493
3903
  }
3494
3904
  return { context, browser, chromeSession };
3495
3905
  }
@@ -3506,23 +3916,23 @@ async function cleanupBrowserSession(session) {
3506
3916
  init_lib();
3507
3917
 
3508
3918
  // src/browser/dry-run.ts
3509
- var import_node_path12 = require("path");
3919
+ var import_node_path13 = require("path");
3510
3920
  init_utils();
3511
3921
 
3512
3922
  // src/browser/step-validator.ts
3513
- var import_node_fs7 = require("fs");
3514
- var import_node_path11 = require("path");
3923
+ var import_node_fs8 = require("fs");
3924
+ var import_node_path12 = require("path");
3515
3925
  init_utils();
3516
3926
  function findMediaFile(filename, baseDir) {
3517
- const name = (0, import_node_path11.basename)(filename);
3518
- const searchDirs = [baseDir, (0, import_node_path11.join)(baseDir, "res"), (0, import_node_path11.join)(baseDir, "assets"), (0, import_node_path11.join)(baseDir, "media")];
3927
+ const name = (0, import_node_path12.basename)(filename);
3928
+ const searchDirs = [baseDir, (0, import_node_path12.join)(baseDir, "res"), (0, import_node_path12.join)(baseDir, "assets"), (0, import_node_path12.join)(baseDir, "media")];
3519
3929
  for (const dir of searchDirs) {
3520
- if (!(0, import_node_fs7.existsSync)(dir)) continue;
3930
+ if (!(0, import_node_fs8.existsSync)(dir)) continue;
3521
3931
  try {
3522
- const files = (0, import_node_fs7.readdirSync)(dir);
3523
- if (files.includes(name)) return (0, import_node_path11.join)(dir, name);
3932
+ const files = (0, import_node_fs8.readdirSync)(dir);
3933
+ if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
3524
3934
  const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
3525
- if (match) return (0, import_node_path11.join)(dir, match);
3935
+ if (match) return (0, import_node_path12.join)(dir, match);
3526
3936
  } catch {
3527
3937
  }
3528
3938
  }
@@ -3594,8 +4004,8 @@ function resolveMediaStep(step, baseDir, label, startTime) {
3594
4004
  if (!step.value) {
3595
4005
  return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
3596
4006
  }
3597
- const specifiedPath = (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value);
3598
- if ((0, import_node_fs7.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
4007
+ const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
4008
+ if ((0, import_node_fs8.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
3599
4009
  const foundPath = findMediaFile(step.value, baseDir);
3600
4010
  if (foundPath) {
3601
4011
  step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -3716,8 +4126,8 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
3716
4126
  const isPlaceholder = !rawPath || rawPath.startsWith("[") || rawPath.includes("example");
3717
4127
  if (!isPlaceholder) {
3718
4128
  const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
3719
- let filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
3720
- if (!(0, import_node_fs7.existsSync)(filePath)) {
4129
+ let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
4130
+ if (!(0, import_node_fs8.existsSync)(filePath)) {
3721
4131
  const foundPath = findMediaFile(rawPath, baseDir);
3722
4132
  if (foundPath) {
3723
4133
  const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
@@ -3816,8 +4226,13 @@ async function getPageElements(page) {
3816
4226
  });
3817
4227
  }
3818
4228
  async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
3819
- const userDataDir = (0, import_node_path12.join)(baseDir, ".browser-data");
3820
- const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir);
4229
+ const userDataDir = (0, import_node_path13.join)(baseDir, ".browser-data");
4230
+ const { context, browser, chromeSession } = await launchBrowserContext(
4231
+ config,
4232
+ userDataDir,
4233
+ void 0,
4234
+ findAuthState(plan.url)
4235
+ );
3821
4236
  const page = context.pages()[0] || await context.newPage();
3822
4237
  setupDownloadTracking(page);
3823
4238
  const results = [];
@@ -3876,27 +4291,27 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
3876
4291
  }
3877
4292
 
3878
4293
  // src/browser/meta.ts
3879
- var import_node_fs8 = require("fs");
3880
- var import_promises8 = require("fs/promises");
3881
- var import_node_path13 = require("path");
4294
+ var import_node_fs9 = require("fs");
4295
+ var import_promises9 = require("fs/promises");
4296
+ var import_node_path14 = require("path");
3882
4297
  var RECORDING_FILE = "recording.mp4";
3883
4298
  var RECORDING_META_FILE = "recording.meta.json";
3884
4299
  function getRecordingPath(outputDir) {
3885
- return (0, import_node_path13.join)(outputDir, RECORDING_FILE);
4300
+ return (0, import_node_path14.join)(outputDir, RECORDING_FILE);
3886
4301
  }
3887
4302
  async function loadRecordingMeta(outputDir) {
3888
- const metaPath = (0, import_node_path13.join)(outputDir, RECORDING_META_FILE);
3889
- if (!(0, import_node_fs8.existsSync)(metaPath)) return null;
4303
+ const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4304
+ if (!(0, import_node_fs9.existsSync)(metaPath)) return null;
3890
4305
  try {
3891
- const content = await (0, import_promises8.readFile)(metaPath, "utf-8");
4306
+ const content = await (0, import_promises9.readFile)(metaPath, "utf-8");
3892
4307
  return JSON.parse(content);
3893
4308
  } catch {
3894
4309
  return null;
3895
4310
  }
3896
4311
  }
3897
4312
  async function saveRecordingMeta(outputDir, meta) {
3898
- const metaPath = (0, import_node_path13.join)(outputDir, RECORDING_META_FILE);
3899
- await (0, import_promises8.writeFile)(metaPath, JSON.stringify(meta, null, 2));
4313
+ const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
4314
+ await (0, import_promises9.writeFile)(metaPath, JSON.stringify(meta, null, 2));
3900
4315
  }
3901
4316
  function canReuseRecording(plan, config, stepDurations, existingMeta) {
3902
4317
  if (!existingMeta) {
@@ -3965,16 +4380,17 @@ function formatStepLabel(step) {
3965
4380
  return `${caption.slice(0, 47)}...`;
3966
4381
  }
3967
4382
  async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
3968
- const videoDir = (0, import_node_path14.join)(workDir, "video");
3969
- await (0, import_promises9.mkdir)(videoDir, { recursive: true });
4383
+ const videoDir = (0, import_node_path15.join)(workDir, "video");
4384
+ await (0, import_promises10.mkdir)(videoDir, { recursive: true });
3970
4385
  const getStepDuration = (stepNum) => {
3971
4386
  return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
3972
4387
  };
3973
4388
  const browserTask = task2("Starting browser recording");
3974
4389
  const recordingStartTime = Date.now();
3975
- const userDataDir = (0, import_node_path14.join)(workDir, "browser-data");
4390
+ const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
3976
4391
  const recordVideo = { dir: videoDir, size: { width: config.video.width, height: config.video.height } };
3977
- const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo);
4392
+ const authState = findAuthState(plan.url);
4393
+ const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
3978
4394
  const page = context.pages()[0] || await context.newPage();
3979
4395
  setupDownloadTracking(page);
3980
4396
  context.on("page", (p) => setupDownloadTracking(p));
@@ -4072,9 +4488,9 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4072
4488
  actualStepDurations.push(actualDuration);
4073
4489
  }
4074
4490
  if (failures.length > 0) {
4075
- console.log(import_chalk3.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
4491
+ console.log(import_chalk6.default.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
4076
4492
  for (const f of failures) {
4077
- console.log(import_chalk3.default.yellow(` - ${f}`));
4493
+ console.log(import_chalk6.default.yellow(` - ${f}`));
4078
4494
  }
4079
4495
  }
4080
4496
  const saveTask = task2("Saving video");
@@ -4083,7 +4499,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4083
4499
  await page.close();
4084
4500
  const videoPath = await video.path();
4085
4501
  await cleanupBrowserSession({ context, browser, chromeSession });
4086
- if (!videoPath || !(0, import_node_fs9.existsSync)(videoPath)) {
4502
+ if (!videoPath || !(0, import_node_fs10.existsSync)(videoPath)) {
4087
4503
  throw new Error("Video file was not created");
4088
4504
  }
4089
4505
  saveTask.succeed("Video saved");
@@ -4091,33 +4507,18 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
4091
4507
  }
4092
4508
 
4093
4509
  // src/utils/video-pipeline.ts
4094
- var import_node_fs10 = require("fs");
4095
- var import_promises10 = require("fs/promises");
4096
- var import_node_path15 = require("path");
4510
+ var import_video_kit6 = require("@spark-apps/video-kit");
4511
+ var import_node_fs11 = require("fs");
4512
+ var import_promises11 = require("fs/promises");
4513
+ var import_node_path16 = require("path");
4514
+ var import_chalk7 = __toESM(require("chalk"));
4097
4515
  init_lib();
4098
4516
  var INDENT2 = " ";
4099
- function chunkWords(words, maxWords) {
4100
- const chunks = [];
4101
- for (let i = 0; i < words.length; i += maxWords) {
4102
- chunks.push(words.slice(i, i + maxWords));
4103
- }
4104
- return chunks;
4105
- }
4106
- function formatASSTime(ms) {
4107
- const hours = Math.floor(ms / 36e5);
4108
- const minutes = Math.floor(ms % 36e5 / 6e4);
4109
- const seconds = Math.floor(ms % 6e4 / 1e3);
4110
- const centis = Math.floor(ms % 1e3 / 10);
4111
- return `${hours}:${pad(minutes)}:${pad(seconds)}.${pad(centis)}`;
4112
- }
4113
- function pad(n, len = 2) {
4114
- return n.toString().padStart(len, "0");
4115
- }
4116
4517
  async function generateASS(plan, stepAudio, workDir, config, captions) {
4117
- const regularColor = hexToASS(captions.regular.color);
4118
- const highlightColor = hexToASS(captions.highlight.color);
4119
- const outline = hexToASS(captions.outline);
4120
- const back = hexToASS(captions.outline, 128);
4518
+ const regularColor = (0, import_video_kit.hexToAss)(captions.regular.color);
4519
+ const highlightColor = (0, import_video_kit.hexToAss)(captions.highlight.color);
4520
+ const outline = (0, import_video_kit.hexToAss)(captions.outline);
4521
+ const back = (0, import_video_kit.hexToAss)(captions.outline, 128);
4121
4522
  const highlightScale = Math.round(captions.highlight.size / captions.regular.size * 100);
4122
4523
  let ass = generateASSHeader(
4123
4524
  config.video.width,
@@ -4142,7 +4543,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
4142
4543
  continue;
4143
4544
  }
4144
4545
  const words = audio.words.length > 0 ? audio.words : estimateWordTimings(step.caption || "", audio.duration);
4145
- const chunks = chunkWords(words, 3);
4546
+ const chunks = (0, import_video_kit6.chunkWordsByCount)(words, 3);
4146
4547
  for (const chunk of chunks) {
4147
4548
  if (chunk.length === 0) continue;
4148
4549
  const chunkStart = currentTime + chunk[0].start;
@@ -4153,13 +4554,13 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
4153
4554
  const duration = Math.round((word.end - word.start) / 10);
4154
4555
  karaokeText += `{\\kf${duration}\\fscx${highlightScale}\\fscy${highlightScale}}${word.word}{\\fscx100\\fscy100} `;
4155
4556
  }
4156
- ass += `Dialogue: 0,${formatASSTime(chunkStart)},${formatASSTime(chunkEnd)},Karaoke,,0,0,0,,${karaokeText.trim()}
4557
+ ass += `Dialogue: 0,${(0, import_video_kit6.assTimeFromMs)(chunkStart)},${(0, import_video_kit6.assTimeFromMs)(chunkEnd)},Karaoke,,0,0,0,,${karaokeText.trim()}
4157
4558
  `;
4158
4559
  }
4159
4560
  currentTime += audio.duration;
4160
4561
  }
4161
- const assPath = (0, import_node_path15.join)(workDir, "subtitles.ass");
4162
- await (0, import_promises10.writeFile)(assPath, ass);
4562
+ const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
4563
+ await (0, import_promises11.writeFile)(assPath, ass);
4163
4564
  return assPath;
4164
4565
  }
4165
4566
  function buildVideoOverlays(steps, stepDurations, baseDir) {
@@ -4172,8 +4573,8 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
4172
4573
  if ((step.action === "video" || step.action === "media") && step.value) {
4173
4574
  const ext = step.value.split(".").pop()?.toLowerCase() || "";
4174
4575
  if (["mp4", "webm", "mov"].includes(ext)) {
4175
- const mediaPath = (0, import_node_path15.isAbsolute)(step.value) ? step.value : (0, import_node_path15.resolve)(baseDir, step.value);
4176
- if ((0, import_node_fs10.existsSync)(mediaPath)) {
4576
+ const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
4577
+ if ((0, import_node_fs11.existsSync)(mediaPath)) {
4177
4578
  const partial = parsePartial(step.partial);
4178
4579
  overlays.push({
4179
4580
  path: mediaPath,
@@ -4192,23 +4593,23 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
4192
4593
  }
4193
4594
  async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
4194
4595
  header("Compose");
4195
- const configPath = (0, import_node_path15.join)(process.cwd(), CONFIG_FILE);
4196
- console.log(`${INDENT2}${require("chalk").dim(`Config: ${formatClickablePath(configPath)}`)}`);
4596
+ const configPath = (0, import_node_path16.join)(process.cwd(), CONFIG_FILE);
4597
+ console.log(`${INDENT2}${import_chalk7.default.dim(`Config: ${formatClickablePath(configPath)}`)}`);
4197
4598
  const proceed = await confirm(`${INDENT2}Review config or continue?`);
4198
4599
  if (!proceed) {
4199
- console.log(require("chalk").yellow(`
4600
+ console.log(import_chalk7.default.yellow(`
4200
4601
  Edit ${CONFIG_FILE} and run again.
4201
4602
  `));
4202
4603
  process.exit(0);
4203
4604
  }
4204
4605
  const composeTask = task("Building final video");
4205
- await (0, import_promises10.mkdir)(config.output.dir, { recursive: true });
4606
+ await (0, import_promises11.mkdir)(config.output.dir, { recursive: true });
4206
4607
  const suffix = config.lang ? `-${config.lang}` : "";
4207
- const finalPath = (0, import_node_path15.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
4608
+ const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
4208
4609
  const videoDuration = await getMediaDuration(videoPath) / 1e3;
4209
- const hasVoiceover = audioPath && (0, import_node_fs10.existsSync)(audioPath);
4210
- 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) : "";
4211
- const hasMusic = musicPath && (0, import_node_fs10.existsSync)(musicPath);
4610
+ const hasVoiceover = audioPath && (0, import_node_fs11.existsSync)(audioPath);
4611
+ const musicPath = config.music?.path ? (0, import_node_path16.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path16.resolve)(config.output.dir, config.music.path) : "";
4612
+ const hasMusic = musicPath && (0, import_node_fs11.existsSync)(musicPath);
4212
4613
  try {
4213
4614
  await composeVideo({
4214
4615
  videoPath,
@@ -4245,10 +4646,9 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
4245
4646
  async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
4246
4647
  const duration = await getMediaDuration(finalPath);
4247
4648
  if (duration <= limitSecs * 1e3) return finalPath;
4248
- const chalk17 = require("chalk");
4249
4649
  const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
4250
4650
  console.log();
4251
- console.log(chalk17.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
4651
+ console.log(import_chalk7.default.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
4252
4652
  const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
4253
4653
  if (!speedUp) return finalPath;
4254
4654
  const speedTask = task(`Speeding up ${speedFactor}x`);
@@ -4270,23 +4670,23 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
4270
4670
  }
4271
4671
 
4272
4672
  // src/commands/utils/record-helpers.ts
4273
- var import_chalk5 = __toESM(require("chalk"));
4673
+ var import_chalk9 = __toESM(require("chalk"));
4274
4674
  init_lib();
4275
4675
 
4276
4676
  // src/plan.ts
4277
- var import_promises11 = require("fs/promises");
4278
- var import_node_path16 = require("path");
4279
- var import_chalk4 = __toESM(require("chalk"));
4280
- var import_playwright5 = require("playwright");
4677
+ var import_promises12 = require("fs/promises");
4678
+ var import_node_path17 = require("path");
4679
+ var import_chalk8 = __toESM(require("chalk"));
4680
+ var import_playwright6 = require("playwright");
4281
4681
  init_lib();
4282
4682
  async function crawlExtension(url, extensionPath, workDir, task2) {
4283
4683
  const browserTask = task2("Launching browser with extension");
4284
- const userDataDir = (0, import_node_path16.join)(workDir, ".browser-data");
4285
- await (0, import_promises11.mkdir)(userDataDir, { recursive: true });
4684
+ const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
4685
+ await (0, import_promises12.mkdir)(userDataDir, { recursive: true });
4286
4686
  let context;
4287
4687
  let chromeSession = null;
4288
4688
  const useWindowsChrome = isWSL() && tryEnableWSLInterop();
4289
- const launchPersistent = () => import_playwright5.chromium.launchPersistentContext(userDataDir, {
4689
+ const launchPersistent = () => import_playwright6.chromium.launchPersistentContext(userDataDir, {
4290
4690
  headless: false,
4291
4691
  args: [
4292
4692
  `--disable-extensions-except=${extensionPath}`,
@@ -4302,7 +4702,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4302
4702
  const contexts = chromeSession.browser.contexts();
4303
4703
  context = contexts[0] || await chromeSession.browser.newContext({ viewport: { width: 400, height: 600 } });
4304
4704
  } catch (_err) {
4305
- console.log(import_chalk4.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
4705
+ console.log(import_chalk8.default.yellow(" \u26A0 Windows Chrome unavailable, using Chromium"));
4306
4706
  context = await launchPersistent();
4307
4707
  }
4308
4708
  } else {
@@ -4379,8 +4779,34 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
4379
4779
  crawlTask.succeed(`Found ${elements.length} interactive elements`);
4380
4780
  return { title, elements };
4381
4781
  }
4782
+ var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
4783
+ var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
4784
+ The JSON should have: url, title, description, steps array.
4785
+ Each step has: id (number), action, caption, and optionally target, value.
4786
+
4787
+ Malformed content:
4788
+ ${content}`;
4789
+ async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
4790
+ try {
4791
+ return JSON.parse(content);
4792
+ } catch {
4793
+ const fixTask = task2("Fixing malformed JSON");
4794
+ try {
4795
+ const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
4796
+ const jsonMatch = fixed.match(/\{[\s\S]*\}/);
4797
+ if (!jsonMatch) throw new Error("No JSON in fix");
4798
+ const plan = JSON.parse(jsonMatch[0]);
4799
+ fixTask.succeed("JSON fixed by AI");
4800
+ return plan;
4801
+ } catch (err) {
4802
+ await logError2("JSON Fix", err);
4803
+ fixTask.fail("Could not fix JSON");
4804
+ throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
4805
+ }
4806
+ }
4807
+ }
4382
4808
  async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
4383
- const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2);
4809
+ const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
4384
4810
  const aiTask = task2("Generating plan with AI");
4385
4811
  const langName = getLanguageName(lang);
4386
4812
  const systemPrompt = buildSystemPrompt(maxSteps);
@@ -4389,7 +4815,7 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
4389
4815
  const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
4390
4816
  const jsonMatch = content.match(/\{[\s\S]*\}/);
4391
4817
  if (!jsonMatch) throw new Error("No JSON found in response");
4392
- const plan = JSON.parse(jsonMatch[0]);
4818
+ const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
4393
4819
  if (lang !== void 0) plan.lang = lang;
4394
4820
  plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
4395
4821
  ...s,
@@ -4406,36 +4832,8 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
4406
4832
  }
4407
4833
  }
4408
4834
  async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
4409
- const content = await (0, import_promises11.readFile)(planPath, "utf-8");
4410
- let plan;
4411
- try {
4412
- plan = JSON.parse(content);
4413
- } catch (_parseError) {
4414
- const fixTask = task2("Fixing malformed JSON");
4415
- checkCredentials();
4416
- try {
4417
- const fixed = await callGroq(
4418
- "You fix malformed JSON. Return ONLY valid JSON, no explanation.",
4419
- `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
4420
- The JSON should have: url, title, description, steps array.
4421
- Each step has: id (number), action, caption, and optionally target, value.
4422
-
4423
- Malformed content:
4424
- ${content}`,
4425
- logError2,
4426
- userEmail,
4427
- tier
4428
- );
4429
- const jsonMatch = fixed.match(/\{[\s\S]*\}/);
4430
- if (!jsonMatch) throw new Error("No JSON in fix");
4431
- plan = JSON.parse(jsonMatch[0]);
4432
- fixTask.succeed("JSON fixed by AI");
4433
- } catch (err) {
4434
- await logError2("JSON Fix", err);
4435
- fixTask.fail("Could not fix JSON");
4436
- throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
4437
- }
4438
- }
4835
+ const content = await (0, import_promises12.readFile)(planPath, "utf-8");
4836
+ const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
4439
4837
  if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
4440
4838
  throw new Error("Invalid plan: missing url or steps");
4441
4839
  }
@@ -4467,7 +4865,7 @@ ${content}`,
4467
4865
  renumberTask.fail("No valid steps");
4468
4866
  throw new Error("Plan has no valid steps");
4469
4867
  }
4470
- await (0, import_promises11.writeFile)(planPath, JSON.stringify(plan, null, 2));
4868
+ await (0, import_promises12.writeFile)(planPath, JSON.stringify(plan, null, 2));
4471
4869
  const removed = originalCount - plan.steps.length;
4472
4870
  renumberTask.succeed(
4473
4871
  removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
@@ -4499,31 +4897,41 @@ function displayRateLimitInfo() {
4499
4897
  const percentage = Math.round(limits.used / limits.limit * 100);
4500
4898
  if (percentage >= 90) {
4501
4899
  console.log();
4502
- console.log(import_chalk5.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
4900
+ console.log(import_chalk9.default.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
4503
4901
  if (limits.tier === "free") {
4504
- console.log(import_chalk5.default.dim(` Upgrade: ${getUpgradeUrl()}`));
4902
+ console.log(import_chalk9.default.dim(` Upgrade: ${getUpgradeUrl()}`));
4505
4903
  }
4506
4904
  } else if (percentage >= 50) {
4507
- console.log(import_chalk5.default.dim(` ${remaining}/${limits.limit} requests remaining`));
4905
+ console.log(import_chalk9.default.dim(` ${remaining}/${limits.limit} requests remaining`));
4508
4906
  }
4509
4907
  }
4510
- function displayUsageStatus(userEmail, userTier, localVideoCount = 0) {
4511
- const tierLabel = userTier === "pro" ? "Pro" : "Free";
4512
- const used = localVideoCount + 1;
4513
- console.log(import_chalk5.default.cyan(` \u2714 ${tierLabel} tier (${used}/${FREE_VIDEO_LIMIT})`));
4908
+ async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
4909
+ const tier = userTier === "pro" ? "pro" : "free";
4910
+ const tierLabel = tier === "pro" ? import_chalk9.default.green("Pro") : "Free";
4911
+ console.log(import_chalk9.default.cyan(` \u2714 ${tierLabel} plan`));
4912
+ console.log(import_chalk9.default.dim(` ${userEmail}`));
4913
+ if (userEmail && userEmail !== "anonymous") {
4914
+ try {
4915
+ const status = await getStatusCached(userEmail);
4916
+ for (const line of formatUsageLines(status, tier)) {
4917
+ console.log(import_chalk9.default.dim(` ${line}`));
4918
+ }
4919
+ } catch {
4920
+ }
4921
+ }
4514
4922
  if (userTier === "free") {
4515
4923
  const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
4516
- console.log(import_chalk5.default.dim(` Upgrade: ${upgradeUrl}`));
4924
+ console.log(import_chalk9.default.dim(` Upgrade: ${upgradeUrl}`));
4517
4925
  }
4518
4926
  }
4519
4927
  function displayPlanSummary(plan) {
4520
4928
  console.log();
4521
- console.log(import_chalk5.default.dim(` ${plan.title || "Untitled"}`));
4522
- console.log(import_chalk5.default.dim(` ${plan.steps?.length || 0} steps`));
4929
+ console.log(import_chalk9.default.dim(` ${plan.title || "Untitled"}`));
4930
+ console.log(import_chalk9.default.dim(` ${plan.steps?.length || 0} steps`));
4523
4931
  for (const step of plan.steps || []) {
4524
4932
  const text = step.caption || step.action || "";
4525
4933
  const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
4526
- console.log(import_chalk5.default.dim(` ${step.id}. ${caption}`));
4934
+ console.log(import_chalk9.default.dim(` ${step.id}. ${caption}`));
4527
4935
  }
4528
4936
  }
4529
4937
  function getOutputDirFromUrl(url) {
@@ -4589,8 +4997,8 @@ async function runVideoPhase(params) {
4589
4997
  if (trimStart2 !== void 0 && trimEnd !== void 0) {
4590
4998
  mediaDuration = (trimEnd - trimStart2) * 1e3;
4591
4999
  } else {
4592
- const mediaPath = (0, import_node_path17.resolve)(s.value);
4593
- if ((0, import_node_fs11.existsSync)(mediaPath)) {
5000
+ const mediaPath = (0, import_node_path18.resolve)(s.value);
5001
+ if ((0, import_node_fs12.existsSync)(mediaPath)) {
4594
5002
  try {
4595
5003
  const fullDuration = await getMediaDuration(mediaPath);
4596
5004
  const start = (trimStart2 ?? 0) * 1e3;
@@ -4601,8 +5009,8 @@ async function runVideoPhase(params) {
4601
5009
  }
4602
5010
  }
4603
5011
  } else {
4604
- const mediaPath = (0, import_node_path17.resolve)(s.value);
4605
- if ((0, import_node_fs11.existsSync)(mediaPath)) {
5012
+ const mediaPath = (0, import_node_path18.resolve)(s.value);
5013
+ if ((0, import_node_fs12.existsSync)(mediaPath)) {
4606
5014
  try {
4607
5015
  mediaDuration = await getMediaDuration(mediaPath);
4608
5016
  } catch {
@@ -4615,7 +5023,7 @@ async function runVideoPhase(params) {
4615
5023
  })
4616
5024
  );
4617
5025
  const recordingPath = getRecordingPath(config.output.dir);
4618
- const hasExistingRecording = existingRecordingMeta && (0, import_node_fs11.existsSync)(recordingPath);
5026
+ const hasExistingRecording = existingRecordingMeta && (0, import_node_fs12.existsSync)(recordingPath);
4619
5027
  let videoPath = recordingPath;
4620
5028
  const firstStep = plan.steps[0];
4621
5029
  const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
@@ -4624,7 +5032,7 @@ async function runVideoPhase(params) {
4624
5032
  header("Record");
4625
5033
  const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
4626
5034
  stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
4627
- await (0, import_promises12.copyFile)(result.videoPath, recordingPath);
5035
+ await (0, import_promises13.copyFile)(result.videoPath, recordingPath);
4628
5036
  const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
4629
5037
  meta.pageLoadTime = result.pageLoadTime;
4630
5038
  await saveRecordingMeta(config.output.dir, meta);
@@ -4655,7 +5063,7 @@ async function runVideoPhase(params) {
4655
5063
  if (videoOverlays.length > 0) {
4656
5064
  const spliceTask = task("Splicing videos into recording");
4657
5065
  try {
4658
- const splicedPath = (0, import_node_path17.join)(workDir, "spliced.mp4");
5066
+ const splicedPath = (0, import_node_path18.join)(workDir, "spliced.mp4");
4659
5067
  await spliceVideos({
4660
5068
  inputPath: videoPath,
4661
5069
  outputPath: splicedPath,
@@ -4675,7 +5083,7 @@ async function runVideoPhase(params) {
4675
5083
  if (stepTransitions.length > 0) {
4676
5084
  const transitionTask = task("Applying transitions");
4677
5085
  try {
4678
- const transitionedPath = (0, import_node_path17.join)(workDir, "transitioned.mp4");
5086
+ const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
4679
5087
  const videoDuration = alignedStepDurations.reduce((a, b) => a + b, 0) / 1e3;
4680
5088
  await applyTransitions({
4681
5089
  inputVideo: videoPath,
@@ -4696,15 +5104,15 @@ async function runVideoPhase(params) {
4696
5104
  let mainContentDuration;
4697
5105
  if (userTier === "free") {
4698
5106
  const scriptDir = getScriptDir();
4699
- let outroPath = (0, import_node_path17.join)(scriptDir, "assets", "qp.wm");
4700
- if (!(0, import_node_fs11.existsSync)(outroPath)) {
4701
- outroPath = (0, import_node_path17.join)(scriptDir, "..", "dist", "assets", "qp.wm");
5107
+ let outroPath = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
5108
+ if (!(0, import_node_fs12.existsSync)(outroPath)) {
5109
+ outroPath = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
4702
5110
  }
4703
- if ((0, import_node_fs11.existsSync)(outroPath)) {
5111
+ if ((0, import_node_fs12.existsSync)(outroPath)) {
4704
5112
  try {
4705
5113
  const mainDuration = await getMediaDuration(videoPath);
4706
5114
  mainContentDuration = mainDuration / 1e3 - trimStart;
4707
- const combinedPath = (0, import_node_path17.join)(workDir, "combined-with-outro.mp4");
5115
+ const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
4708
5116
  await concatVideoWithOutro({
4709
5117
  inputPath: videoPath,
4710
5118
  outroPath,
@@ -4751,19 +5159,19 @@ async function runVideoPhase(params) {
4751
5159
  }
4752
5160
 
4753
5161
  // src/commands/phases/voiceover-phase.ts
4754
- var import_node_fs12 = require("fs");
4755
- var import_node_path18 = require("path");
4756
- var import_chalk6 = __toESM(require("chalk"));
5162
+ var import_node_fs13 = require("fs");
5163
+ var import_node_path19 = require("path");
5164
+ var import_chalk10 = __toESM(require("chalk"));
4757
5165
  init_lib();
4758
5166
  var INDENT5 = " ";
4759
5167
  async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
4760
5168
  if (fresh) {
4761
5169
  const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
4762
5170
  for (const file of filesToDelete) {
4763
- const filePath = (0, import_node_path18.join)(config.output.dir, file);
4764
- if ((0, import_node_fs12.existsSync)(filePath)) {
4765
- const { unlink: unlink2 } = await import("fs/promises");
4766
- await unlink2(filePath).catch(() => {
5171
+ const filePath = (0, import_node_path19.join)(config.output.dir, file);
5172
+ if ((0, import_node_fs13.existsSync)(filePath)) {
5173
+ const { unlink: unlink3 } = await import("fs/promises");
5174
+ await unlink3(filePath).catch(() => {
4767
5175
  });
4768
5176
  }
4769
5177
  }
@@ -4771,8 +5179,8 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
4771
5179
  }
4772
5180
  const voice = resolveVoice(config);
4773
5181
  const existingMeta = await loadVoiceoverMeta(config.output.dir);
4774
- const voiceoverFile = (0, import_node_path18.join)(config.output.dir, "voiceover.wav");
4775
- const hasExistingVoiceover = existingMeta && (0, import_node_fs12.existsSync)(voiceoverFile);
5182
+ const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
5183
+ const hasExistingVoiceover = existingMeta && (0, import_node_fs13.existsSync)(voiceoverFile);
4776
5184
  const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
4777
5185
  let voiceoverPath;
4778
5186
  let stepAudio;
@@ -4795,9 +5203,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
4795
5203
  const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
4796
5204
  if (reuseCheck.canReuse) {
4797
5205
  console.log();
4798
- console.log(import_chalk6.default.dim(` Found existing voiceover in ${config.output.dir}`));
5206
+ console.log(import_chalk10.default.dim(` Found existing voiceover in ${config.output.dir}`));
4799
5207
  if (reuseCheck.reason) {
4800
- console.log(import_chalk6.default.dim(` ${reuseCheck.reason}`));
5208
+ console.log(import_chalk10.default.dim(` ${reuseCheck.reason}`));
4801
5209
  }
4802
5210
  const reuse = await confirm(`${INDENT5}Reuse existing voiceover?`);
4803
5211
  if (reuse) {
@@ -4837,7 +5245,7 @@ async function captureCommand(url, lang) {
4837
5245
  const captureResult = await captureUserSession(url, workDir);
4838
5246
  info(`Captured ${captureResult.events.length} interactions over ${(captureResult.durationMs / 1e3).toFixed(0)}s`);
4839
5247
  if (captureResult.events.length === 0) {
4840
- console.log(import_chalk7.default.yellow(" No interactions captured. Exiting."));
5248
+ console.log(import_chalk11.default.yellow(" No interactions captured. Exiting."));
4841
5249
  return;
4842
5250
  }
4843
5251
  header("Convert");
@@ -4845,7 +5253,7 @@ async function captureCommand(url, lang) {
4845
5253
  const rawSteps = eventsToSteps(captureResult.events, url);
4846
5254
  if (rawSteps.length === 0) {
4847
5255
  convertTask.fail("No actionable steps found");
4848
- console.log(import_chalk7.default.yellow(" No actionable steps could be derived from the captured events."));
5256
+ console.log(import_chalk11.default.yellow(" No actionable steps could be derived from the captured events."));
4849
5257
  return;
4850
5258
  }
4851
5259
  convertTask.succeed(`${rawSteps.length} steps extracted`);
@@ -4877,16 +5285,16 @@ async function captureCommand(url, lang) {
4877
5285
  ...lang !== void 0 ? { lang } : {},
4878
5286
  steps
4879
5287
  };
4880
- const planPath = (0, import_node_path19.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
5288
+ const planPath = (0, import_node_path20.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
4881
5289
  await savePlan(plan, planPath);
4882
5290
  console.log();
4883
- console.log(import_chalk7.default.bold.green(` Plan saved: ${planPath}`));
4884
- console.log(import_chalk7.default.dim(` Run: qp ${url} to replay and create a polished demo video`));
5291
+ console.log(import_chalk11.default.bold.green(` Plan saved: ${planPath}`));
5292
+ console.log(import_chalk11.default.dim(` Run: qp ${url} to replay and create a polished demo video`));
4885
5293
  console.log();
4886
5294
  const makeVideo = await confirm(" Create polished demo video now?");
4887
5295
  if (!makeVideo) {
4888
5296
  console.log();
4889
- console.log(import_chalk7.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
5297
+ console.log(import_chalk11.default.dim(` Run: qp ${url} anytime to create a video from this plan.`));
4890
5298
  return;
4891
5299
  }
4892
5300
  header("Voiceover");
@@ -4902,20 +5310,20 @@ async function captureCommand(url, lang) {
4902
5310
  userTier
4903
5311
  });
4904
5312
  console.log();
4905
- console.log(import_chalk7.default.bold.green(" Done!"));
4906
- console.log(import_chalk7.default.cyan(` ${finalPath}`));
5313
+ console.log(import_chalk11.default.bold.green(" Done!"));
5314
+ console.log(import_chalk11.default.cyan(` ${finalPath}`));
4907
5315
  }
4908
5316
 
4909
5317
  // src/commands/init-cmd.ts
4910
- var import_node_fs13 = require("fs");
4911
- var import_promises13 = require("fs/promises");
4912
- var import_node_path20 = require("path");
4913
- var import_chalk8 = __toESM(require("chalk"));
5318
+ var import_node_fs14 = require("fs");
5319
+ var import_promises14 = require("fs/promises");
5320
+ var import_node_path21 = require("path");
5321
+ var import_chalk12 = __toESM(require("chalk"));
4914
5322
  init_lib();
4915
5323
  async function initCommand() {
4916
- const configPath = (0, import_node_path20.join)(process.cwd(), CONFIG_FILE);
4917
- if ((0, import_node_fs13.existsSync)(configPath)) {
4918
- console.log(import_chalk8.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
5324
+ const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
5325
+ if ((0, import_node_fs14.existsSync)(configPath)) {
5326
+ console.log(import_chalk12.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
4919
5327
  return;
4920
5328
  }
4921
5329
  const defaultConfig = {
@@ -4938,32 +5346,32 @@ async function initCommand() {
4938
5346
  volume: 0.3
4939
5347
  }
4940
5348
  };
4941
- await (0, import_promises13.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
5349
+ await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
4942
5350
  `);
4943
- console.log(import_chalk8.default.green(` \u2714 Created ${CONFIG_FILE}`));
4944
- console.log(import_chalk8.default.dim(` Edit it or use: qp config <key> <value>`));
5351
+ console.log(import_chalk12.default.green(` \u2714 Created ${CONFIG_FILE}`));
5352
+ console.log(import_chalk12.default.dim(` Edit it or use: qp config <key> <value>`));
4945
5353
  }
4946
5354
  async function configCommand(key, value) {
4947
- const configPath = (0, import_node_path20.join)(process.cwd(), CONFIG_FILE);
5355
+ const configPath = (0, import_node_path21.join)(process.cwd(), CONFIG_FILE);
4948
5356
  if (!key) {
4949
- if (!(0, import_node_fs13.existsSync)(configPath)) {
4950
- console.log(import_chalk8.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
5357
+ if (!(0, import_node_fs14.existsSync)(configPath)) {
5358
+ console.log(import_chalk12.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
4951
5359
  return;
4952
5360
  }
4953
- const content = await (0, import_promises13.readFile)(configPath, "utf-8");
4954
- console.log(import_chalk8.default.cyan(` ${CONFIG_FILE}:`));
5361
+ const content = await (0, import_promises14.readFile)(configPath, "utf-8");
5362
+ console.log(import_chalk12.default.cyan(` ${CONFIG_FILE}:`));
4955
5363
  console.log(content);
4956
5364
  return;
4957
5365
  }
4958
5366
  if (!value) {
4959
- console.log(import_chalk8.default.red(` Missing value for ${key}`));
4960
- console.log(import_chalk8.default.dim(` Usage: qp config ${key} <value>`));
5367
+ console.log(import_chalk12.default.red(` Missing value for ${key}`));
5368
+ console.log(import_chalk12.default.dim(` Usage: qp config ${key} <value>`));
4961
5369
  return;
4962
5370
  }
4963
5371
  let config = {};
4964
- if ((0, import_node_fs13.existsSync)(configPath)) {
5372
+ if ((0, import_node_fs14.existsSync)(configPath)) {
4965
5373
  try {
4966
- config = JSON.parse(await (0, import_promises13.readFile)(configPath, "utf-8"));
5374
+ config = JSON.parse(await (0, import_promises14.readFile)(configPath, "utf-8"));
4967
5375
  } catch {
4968
5376
  }
4969
5377
  }
@@ -4985,124 +5393,101 @@ async function configCommand(key, value) {
4985
5393
  }
4986
5394
  obj[keys[keys.length - 1]] = parsedValue;
4987
5395
  }
4988
- await (0, import_promises13.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
5396
+ await (0, import_promises14.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
4989
5397
  `);
4990
- console.log(import_chalk8.default.green(` \u2714 Set ${key} = ${value}`));
5398
+ console.log(import_chalk12.default.green(` \u2714 Set ${key} = ${value}`));
4991
5399
  }
4992
5400
 
4993
- // src/commands/record-cmd.ts
4994
- var import_node_fs16 = require("fs");
4995
- var import_promises16 = require("fs/promises");
4996
- var import_node_path23 = require("path");
4997
- var import_chalk14 = __toESM(require("chalk"));
4998
-
4999
- // src/help.ts
5000
- var import_chalk9 = __toESM(require("chalk"));
5401
+ // src/commands/login-cmd.ts
5402
+ var import_chalk13 = __toESM(require("chalk"));
5001
5403
  init_lib();
5002
-
5003
- // src/logo.ts
5004
- var import_figlet = __toESM(require("figlet"));
5005
- var import_gradient_string = __toESM(require("gradient-string"));
5006
- var DEFAULT_LOGO_CONFIG = {
5007
- text: "QuickPeek",
5008
- font: "Slant",
5009
- horizontalLayout: "default",
5010
- gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
5011
- suffix: " \u{1F440}"
5012
- };
5013
- function renderLogo(config = {}) {
5014
- const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
5015
- const ascii = import_figlet.default.textSync(resolved.text, {
5016
- font: resolved.font,
5017
- horizontalLayout: resolved.horizontalLayout
5018
- });
5019
- const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
5020
- return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
5021
- }
5022
-
5023
- // src/help.ts
5024
- var cmd = import_chalk9.default.cyan;
5025
- var arg = import_chalk9.default.green;
5026
- var opt = import_chalk9.default.magenta;
5027
- var desc = import_chalk9.default.white;
5028
- var ex = import_chalk9.default.dim;
5029
- function pad2(colored, visible, width) {
5030
- return colored + " ".repeat(Math.max(0, width - visible.length));
5031
- }
5032
- function showLogo() {
5033
- const logo = renderLogo();
5034
- console.log(logo);
5035
- console.log(import_chalk9.default.dim(` v${VERSION} - Auto-generate demo videos with AI`));
5036
- console.log(import_chalk9.default.italic.dim(" Because Tutorial Videos are Overrated!\n"));
5037
- }
5038
- function showHelp() {
5039
- showLogo();
5040
- const W = 28;
5041
- const usage = [
5042
- [`${cmd("qp")} ${arg("<url>")}`, `qp <url>`, "AI-generated demo video"],
5043
- [`${cmd("qp")} ${arg("<url>")} ${opt("--script")} ${arg("<file>")}`, `qp <url> --script <file>`, "Use your own narration script"],
5044
- [`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
5045
- [`${cmd("qp init")}`, `qp init`, "Create config file"],
5046
- [`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
5047
- ];
5048
- const options = [
5049
- [`${opt("lang=")}${arg("xx")}`, `lang=xx`, "Voice language (en, es, ar, zh, fr, de, ja...)"],
5050
- [`${opt("-d")}, ${opt("--description")} ${arg('"..."')}`, `-d, --description "..."`, "Guide AI with an app description"],
5051
- [`${opt("-s")}, ${opt("--script")} ${arg("<file>")}`, `-s, --script <file>`, "Narration script (one caption per line)"],
5052
- [`${opt("-y")}, ${opt("--yes")}`, `-y, --yes`, "Skip confirmation prompts"],
5053
- [`${opt("--skip-voice")}, ${opt("-sv")}`, `--skip-voice, -sv`, "Reuse existing voiceover"],
5054
- [`${opt("-f")}, ${opt("--fresh")}`, `-f, --fresh`, "Clear cached voiceover and recording"]
5055
- ];
5056
- const examples = [
5057
- `${ex("$")} ${cmd("qp")} ${arg("localhost:3000")}`,
5058
- `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("lang=")}${arg("es")} ${opt("-d")} ${arg('"E-commerce checkout flow"')}`,
5059
- `${ex("$")} ${cmd("qp")} ${arg("myapp.com")} ${opt("--script")} ${arg("narration.txt")}`,
5060
- `${ex("$")} ${cmd("qp capture")} ${arg("localhost:3000")}`
5061
- ];
5404
+ async function loginCommand() {
5062
5405
  console.log();
5063
- console.log(import_chalk9.default.yellow.bold("USAGE"));
5064
- for (const [colored, plain, d] of usage) {
5065
- console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
5406
+ showLogo();
5407
+ const existing = await loadUserData();
5408
+ if (existing.email && existing.verified) {
5409
+ console.log(import_chalk13.default.dim(` Already signed in as ${existing.email}`));
5410
+ console.log(import_chalk13.default.dim(" Run ") + import_chalk13.default.cyan("qp logout") + import_chalk13.default.dim(" first to switch accounts."));
5411
+ console.log();
5412
+ try {
5413
+ const status = await getStatusCached(existing.email);
5414
+ const tier2 = await getTierByEmail(existing.email);
5415
+ const tierLabel2 = tier2 === "pro" ? import_chalk13.default.green("Pro") : "Free";
5416
+ console.log(` ${tierLabel2} plan`);
5417
+ for (const line of formatUsageLines(status, tier2)) {
5418
+ console.log(import_chalk13.default.dim(` ${line}`));
5419
+ }
5420
+ } catch {
5421
+ }
5422
+ console.log();
5423
+ return;
5066
5424
  }
5067
- console.log();
5068
- console.log(import_chalk9.default.yellow.bold("OPTIONS"));
5069
- for (const [colored, plain, d] of options) {
5070
- console.log(` ${pad2(colored, plain, W)}${desc(d)}`);
5425
+ console.log(import_chalk13.default.white(" Sign in with your Google account to track usage"));
5426
+ console.log(import_chalk13.default.white(" and unlock Pro features."));
5427
+ const email = await browserLogin();
5428
+ if (!email) {
5429
+ console.log();
5430
+ console.log(import_chalk13.default.yellow(" Sign-in cancelled or timed out."));
5431
+ console.log(import_chalk13.default.dim(" Run ") + import_chalk13.default.cyan("qp login") + import_chalk13.default.dim(" to try again."));
5432
+ console.log();
5433
+ return;
5071
5434
  }
5072
- console.log();
5073
- console.log(import_chalk9.default.yellow.bold("EXAMPLES"));
5074
- for (const e of examples) {
5075
- console.log(` ${e}`);
5435
+ let tier = "free";
5436
+ try {
5437
+ tier = await getTierByEmail(email);
5438
+ } catch {
5076
5439
  }
5440
+ await saveUserData({ email, verified: true, tier });
5441
+ console.log();
5442
+ const tierLabel = tier === "pro" ? import_chalk13.default.green("Pro") : "Free";
5443
+ console.log(import_chalk13.default.green(` \u2713 Signed in as ${email}`));
5444
+ console.log(` ${tierLabel} plan`);
5445
+ console.log();
5446
+ }
5447
+ async function logoutCommand() {
5077
5448
  console.log();
5078
- console.log(import_chalk9.default.yellow.bold("ENVIRONMENT"));
5079
- console.log(` ${pad2(opt("SPARK_API_KEY"), "SPARK_API_KEY", W)}${desc("Spark AI API key")} ${ex("(get at sparkbrain.app/dashboard)")}`);
5449
+ const userData = await loadUserData();
5450
+ if (!userData.email) {
5451
+ console.log(import_chalk13.default.dim(" Not signed in."));
5452
+ console.log();
5453
+ return;
5454
+ }
5455
+ const email = userData.email;
5456
+ await saveUserData({ email: "", verified: false, tier: "free" });
5457
+ console.log(import_chalk13.default.green(` \u2713 Signed out (${email})`));
5080
5458
  console.log();
5081
5459
  }
5082
5460
 
5083
5461
  // src/commands/record-cmd.ts
5462
+ var import_node_fs17 = require("fs");
5463
+ var import_promises17 = require("fs/promises");
5464
+ var import_node_path24 = require("path");
5465
+ var import_chalk18 = __toESM(require("chalk"));
5084
5466
  init_lib();
5085
5467
 
5086
5468
  // src/commands/phases/auth-phase.ts
5087
- var import_chalk11 = __toESM(require("chalk"));
5469
+ var import_chalk15 = __toESM(require("chalk"));
5088
5470
  init_lib();
5089
5471
 
5090
5472
  // src/commands/utils/auth-helpers.ts
5091
- var import_chalk10 = __toESM(require("chalk"));
5473
+ var import_chalk14 = __toESM(require("chalk"));
5092
5474
  init_lib();
5093
5475
  async function handleRegistrationGate() {
5094
5476
  console.log();
5095
- console.log(import_chalk10.default.yellow(" \u{1F389} You've made 5 free videos!"));
5096
- console.log(import_chalk10.default.white(" Register to continue creating videos."));
5097
- console.log();
5098
- console.log(import_chalk10.default.dim(" Benefits of registering:"));
5099
- console.log(import_chalk10.default.dim(" \u2022 20 more videos per month"));
5100
- console.log(import_chalk10.default.dim(" \u2022 Keep your video history"));
5101
- console.log(import_chalk10.default.dim(" \u2022 Upgrade to Pro anytime"));
5477
+ console.log(import_chalk14.default.yellow(" \u{1F389} You've made 5 free videos!"));
5478
+ console.log(import_chalk14.default.white(" Sign in to continue creating videos."));
5102
5479
  console.log();
5103
- const email = await promptInput("Email: ");
5480
+ console.log(import_chalk14.default.dim(" Benefits of signing in:"));
5481
+ console.log(import_chalk14.default.dim(" \u2022 20 more videos per month"));
5482
+ console.log(import_chalk14.default.dim(" \u2022 Keep your video history"));
5483
+ console.log(import_chalk14.default.dim(" \u2022 Upgrade to Pro anytime"));
5484
+ let email = await browserLogin();
5485
+ if (!email) {
5486
+ console.log();
5487
+ email = await promptInput("Email: ");
5488
+ }
5104
5489
  if (!email || !email.includes("@")) {
5105
- console.log(import_chalk10.default.red(" Invalid email. Run qp again to try again."));
5490
+ console.log(import_chalk14.default.red(" Invalid email. Run qp again to try again."));
5106
5491
  return null;
5107
5492
  }
5108
5493
  const registerTask = task("Sending verification email");
@@ -5117,23 +5502,23 @@ async function handleRegistrationGate() {
5117
5502
  }
5118
5503
  registerTask.succeed("Check your email!");
5119
5504
  console.log();
5120
- console.log(import_chalk10.default.cyan(" \u{1F4E7} Verification email sent to:"));
5121
- console.log(import_chalk10.default.white(` ${email}`));
5505
+ console.log(import_chalk14.default.cyan(" \u{1F4E7} Verification email sent to:"));
5506
+ console.log(import_chalk14.default.white(` ${email}`));
5122
5507
  console.log();
5123
- console.log(import_chalk10.default.dim(" Click the link in the email, then press Enter to continue..."));
5508
+ console.log(import_chalk14.default.dim(" Click the link in the email, then press Enter to continue..."));
5124
5509
  await promptInput("");
5125
5510
  const verifyTask = task("Checking verification");
5126
- const status = await checkVerificationStatus(email);
5127
- if (status.verified) {
5511
+ const verified = await isVerified(email);
5512
+ if (verified) {
5128
5513
  verifyTask.succeed("Email verified!");
5129
5514
  return { email, verified: true };
5130
5515
  }
5131
5516
  verifyTask.fail("Not verified yet");
5132
5517
  console.log();
5133
- console.log(import_chalk10.default.yellow(" Email not verified. Check your inbox and spam folder."));
5134
- console.log(import_chalk10.default.dim(" Run qp again after clicking the verification link."));
5518
+ console.log(import_chalk14.default.yellow(" Email not verified. Check your inbox and spam folder."));
5519
+ console.log(import_chalk14.default.dim(" Run qp again after clicking the verification link."));
5135
5520
  console.log();
5136
- console.log(import_chalk10.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5521
+ console.log(import_chalk14.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
5137
5522
  console.log();
5138
5523
  return { email, verified: false };
5139
5524
  }
@@ -5141,27 +5526,27 @@ async function handleRegistrationGate() {
5141
5526
  // src/commands/phases/auth-phase.ts
5142
5527
  async function runAuthPhase(input) {
5143
5528
  let { userEmail, userTier, localVideoCount } = input;
5144
- const { isVerified } = input;
5145
- if (userEmail && isVerified) {
5529
+ const { isVerified: isVerified2 } = input;
5530
+ if (userEmail && isVerified2) {
5146
5531
  const tierTask = task("Checking subscription");
5147
- userTier = await getTier(userEmail);
5532
+ userTier = await getTierByEmail(userEmail);
5148
5533
  tierTask.stop();
5149
- displayUsageStatus(userEmail, userTier, localVideoCount);
5534
+ await displayUsageStatus(userEmail, userTier, localVideoCount);
5150
5535
  if (userTier !== input.userTier) {
5151
5536
  await saveUserData({ tier: userTier });
5152
5537
  }
5153
- } else if (userEmail && !isVerified) {
5538
+ } else if (userEmail && !isVerified2) {
5154
5539
  const verifyTask = task("Checking verification");
5155
- const status = await checkVerificationStatus(userEmail);
5156
- if (status.verified) {
5540
+ const verified = await isVerified(userEmail);
5541
+ if (verified) {
5157
5542
  verifyTask.stop();
5158
- await saveUserData({ verified: true, tier: status.tier });
5159
- userTier = status.tier;
5160
- displayUsageStatus(userEmail, userTier, localVideoCount);
5543
+ userTier = await getTierByEmail(userEmail);
5544
+ await saveUserData({ verified: true, tier: userTier });
5545
+ await displayUsageStatus(userEmail, userTier, localVideoCount);
5161
5546
  } else {
5162
5547
  verifyTask.fail("Email not verified");
5163
5548
  console.log();
5164
- console.log(import_chalk11.default.yellow(" Check your inbox for the verification email."));
5549
+ console.log(import_chalk15.default.yellow(" Check your inbox for the verification email."));
5165
5550
  console.log();
5166
5551
  const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
5167
5552
  if (input2.toLowerCase() === "exit") {
@@ -5181,29 +5566,29 @@ async function runAuthPhase(input) {
5181
5566
  registerTask.stop();
5182
5567
  userEmail = newEmail;
5183
5568
  await saveUserData({ email: newEmail, verified: true });
5184
- displayUsageStatus(userEmail, userTier, localVideoCount);
5569
+ await displayUsageStatus(userEmail, userTier, localVideoCount);
5185
5570
  } else {
5186
5571
  registerTask.succeed("Check your email!");
5187
5572
  await saveUserData({ email: newEmail, verified: false });
5188
5573
  console.log();
5189
- console.log(import_chalk11.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
5190
- console.log(import_chalk11.default.dim(" Click the link in the email, then run qp again."));
5574
+ console.log(import_chalk15.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
5575
+ console.log(import_chalk15.default.dim(" Click the link in the email, then run qp again."));
5191
5576
  console.log();
5192
5577
  gracefulExit();
5193
5578
  return null;
5194
5579
  }
5195
5580
  } else {
5196
5581
  const recheckTask = task("Rechecking verification");
5197
- const recheckStatus = await checkVerificationStatus(userEmail);
5198
- if (recheckStatus.verified) {
5582
+ const recheckVerified = await isVerified(userEmail);
5583
+ if (recheckVerified) {
5199
5584
  recheckTask.stop();
5200
- await saveUserData({ verified: true, tier: recheckStatus.tier });
5201
- userTier = recheckStatus.tier;
5202
- displayUsageStatus(userEmail, userTier, localVideoCount);
5585
+ userTier = await getTierByEmail(userEmail);
5586
+ await saveUserData({ verified: true, tier: userTier });
5587
+ await displayUsageStatus(userEmail, userTier, localVideoCount);
5203
5588
  } else {
5204
5589
  recheckTask.fail("Still not verified");
5205
5590
  console.log();
5206
- console.log(import_chalk11.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5591
+ console.log(import_chalk15.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
5207
5592
  console.log();
5208
5593
  gracefulExit();
5209
5594
  return null;
@@ -5226,28 +5611,34 @@ async function runAuthPhase(input) {
5226
5611
  gracefulExit();
5227
5612
  return null;
5228
5613
  }
5229
- displayUsageStatus(userEmail, userTier, localVideoCount);
5614
+ await displayUsageStatus(userEmail, userTier, localVideoCount);
5230
5615
  } else {
5231
5616
  const remaining = FREE_VIDEO_LIMIT - localVideoCount;
5232
5617
  console.log();
5233
- console.log(import_chalk11.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
5234
- console.log(import_chalk11.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
5618
+ console.log(import_chalk15.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
5619
+ console.log(import_chalk15.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
5235
5620
  userEmail = await setupUserEmail();
5236
- await saveUserData({ email: userEmail, verified: false, localVideoCount });
5621
+ let registeredVerified = false;
5622
+ try {
5623
+ const reg = await registerFreeUser(userEmail);
5624
+ registeredVerified = Boolean(reg.success && reg.alreadyVerified);
5625
+ } catch {
5626
+ }
5627
+ await saveUserData({ email: userEmail, verified: registeredVerified, localVideoCount });
5237
5628
  }
5238
5629
  return {
5239
5630
  userEmail: userEmail ?? "anonymous",
5240
5631
  userTier,
5241
5632
  localVideoCount,
5242
- isVerified
5633
+ isVerified: isVerified2
5243
5634
  };
5244
5635
  }
5245
5636
 
5246
5637
  // src/commands/phases/plan-phase.ts
5247
- var import_node_fs14 = require("fs");
5248
- var import_promises14 = require("fs/promises");
5249
- var import_node_path21 = require("path");
5250
- var import_chalk12 = __toESM(require("chalk"));
5638
+ var import_node_fs15 = require("fs");
5639
+ var import_promises15 = require("fs/promises");
5640
+ var import_node_path22 = require("path");
5641
+ var import_chalk16 = __toESM(require("chalk"));
5251
5642
 
5252
5643
  // src/ai/fix.ts
5253
5644
  init_lib();
@@ -5404,9 +5795,9 @@ async function runPlanPhase(params) {
5404
5795
  const extensionPath = config.browser?.extension;
5405
5796
  let existingPlan = null;
5406
5797
  let existingPlanPath = null;
5407
- if ((0, import_node_fs14.existsSync)(planPath)) {
5798
+ if ((0, import_node_fs15.existsSync)(planPath)) {
5408
5799
  try {
5409
- const parsed = JSON.parse(await (0, import_promises14.readFile)(planPath, "utf-8"));
5800
+ const parsed = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
5410
5801
  if (parsed.url && parsed.steps?.length > 0) {
5411
5802
  existingPlan = parsed;
5412
5803
  existingPlanPath = planPath;
@@ -5416,12 +5807,12 @@ async function runPlanPhase(params) {
5416
5807
  }
5417
5808
  if (!existingPlan) {
5418
5809
  try {
5419
- const files = (0, import_node_fs14.readdirSync)(config.output.dir);
5420
- const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path21.basename)(planPath));
5810
+ const files = (0, import_node_fs15.readdirSync)(config.output.dir);
5811
+ const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path22.basename)(planPath));
5421
5812
  for (const file of planFiles) {
5422
- const filePath = (0, import_node_path21.join)(config.output.dir, file);
5813
+ const filePath = (0, import_node_path22.join)(config.output.dir, file);
5423
5814
  try {
5424
- const parsed = JSON.parse(await (0, import_promises14.readFile)(filePath, "utf-8"));
5815
+ const parsed = JSON.parse(await (0, import_promises15.readFile)(filePath, "utf-8"));
5425
5816
  if (parsed.url && parsed.steps?.length > 0) {
5426
5817
  existingPlan = parsed;
5427
5818
  existingPlanPath = filePath;
@@ -5433,9 +5824,9 @@ async function runPlanPhase(params) {
5433
5824
  } catch {
5434
5825
  }
5435
5826
  }
5436
- if (!existingPlan && (0, import_node_fs14.existsSync)(legacyPlanPath)) {
5827
+ if (!existingPlan && (0, import_node_fs15.existsSync)(legacyPlanPath)) {
5437
5828
  try {
5438
- const parsed = JSON.parse(await (0, import_promises14.readFile)(legacyPlanPath, "utf-8"));
5829
+ const parsed = JSON.parse(await (0, import_promises15.readFile)(legacyPlanPath, "utf-8"));
5439
5830
  if (parsed.url && parsed.steps?.length > 0) {
5440
5831
  existingPlan = parsed;
5441
5832
  existingPlanPath = legacyPlanPath;
@@ -5443,8 +5834,8 @@ async function runPlanPhase(params) {
5443
5834
  } catch {
5444
5835
  }
5445
5836
  }
5446
- const normalizeUrl3 = (u) => u.replace(/\/+$/, "").toLowerCase();
5447
- const urlMatches = existingPlan && normalizeUrl3(existingPlan.url) === normalizeUrl3(url);
5837
+ const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
5838
+ const urlMatches = existingPlan && normalizeUrl4(existingPlan.url) === normalizeUrl4(url);
5448
5839
  let plan;
5449
5840
  const doTranslate = async (failMsg) => {
5450
5841
  const translateTask = task(`Translating to ${targetLang}`);
@@ -5461,23 +5852,23 @@ async function runPlanPhase(params) {
5461
5852
  };
5462
5853
  if (existingPlan && existingPlanPath) {
5463
5854
  console.log();
5464
- console.log(import_chalk12.default.dim(` Found existing plan: `) + import_chalk12.default.cyan(formatClickablePath(existingPlanPath)));
5465
- console.log(import_chalk12.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
5855
+ console.log(import_chalk16.default.dim(` Found existing plan: `) + import_chalk16.default.cyan(formatClickablePath(existingPlanPath)));
5856
+ console.log(import_chalk16.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
5466
5857
  if (!urlMatches) {
5467
- console.log(import_chalk12.default.yellow(` \u26A0 Plan URL differs from argument:`));
5468
- console.log(import_chalk12.default.yellow(` Plan: ${existingPlan.url}`));
5469
- console.log(import_chalk12.default.yellow(` Arg: ${url}`));
5858
+ console.log(import_chalk16.default.yellow(` \u26A0 Plan URL differs from argument:`));
5859
+ console.log(import_chalk16.default.yellow(` Plan: ${existingPlan.url}`));
5860
+ console.log(import_chalk16.default.yellow(` Arg: ${url}`));
5470
5861
  }
5471
5862
  const existingLang = existingPlan.lang || "en";
5472
5863
  if (existingLang !== targetLang) {
5473
- console.log(import_chalk12.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
5864
+ console.log(import_chalk16.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
5474
5865
  }
5475
5866
  const reuse = await confirm(`${INDENT6}Use existing plan?`);
5476
5867
  if (reuse) {
5477
5868
  plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
5478
5869
  if (existingPlanPath !== planPath) {
5479
5870
  await savePlan(plan, planPath);
5480
- console.log(import_chalk12.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
5871
+ console.log(import_chalk16.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
5481
5872
  }
5482
5873
  if (existingLang !== targetLang) {
5483
5874
  const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
@@ -5512,7 +5903,7 @@ async function runPlanPhase(params) {
5512
5903
  uploadIdx++;
5513
5904
  }
5514
5905
  }
5515
- console.log(import_chalk12.default.dim(` Preserved upload paths from previous plan`));
5906
+ console.log(import_chalk16.default.dim(` Preserved upload paths from previous plan`));
5516
5907
  }
5517
5908
  await savePlan(plan, planPath);
5518
5909
  plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
@@ -5536,8 +5927,8 @@ async function runPlanPhase(params) {
5536
5927
  }
5537
5928
  displayPlanSummary(plan);
5538
5929
  console.log();
5539
- console.log(import_chalk12.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5540
- console.log(import_chalk12.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
5930
+ console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5931
+ console.log(import_chalk16.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
5541
5932
  const MAX_REFINEMENTS = 10;
5542
5933
  let refinementCount = 0;
5543
5934
  while (true) {
@@ -5555,15 +5946,15 @@ async function runPlanPhase(params) {
5555
5946
  refineTask.succeed("Plan refined");
5556
5947
  displayRateLimitInfo();
5557
5948
  if (plan.steps.length > maxSteps) {
5558
- console.log(import_chalk12.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
5949
+ console.log(import_chalk16.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
5559
5950
  }
5560
5951
  displayPlanSummary(plan);
5561
5952
  console.log();
5562
- console.log(import_chalk12.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5953
+ console.log(import_chalk16.default.dim(` Plan: ${formatClickablePath(planPath)}`));
5563
5954
  if (refinementCount >= MAX_REFINEMENTS) {
5564
5955
  console.log();
5565
- console.log(import_chalk12.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
5566
- console.log(import_chalk12.default.dim(` Further refinements may incur additional costs.`));
5956
+ console.log(import_chalk16.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
5957
+ console.log(import_chalk16.default.dim(` Further refinements may incur additional costs.`));
5567
5958
  const continueRefining = await confirm(`${INDENT6}Continue refining?`);
5568
5959
  if (!continueRefining) break;
5569
5960
  }
@@ -5575,12 +5966,12 @@ async function runPlanPhase(params) {
5575
5966
  const planLang = plan.lang || "en";
5576
5967
  if (planLang !== targetLang) {
5577
5968
  let useExistingTranslation = false;
5578
- if ((0, import_node_fs14.existsSync)(planPath) && existingPlanPath !== planPath) {
5969
+ if ((0, import_node_fs15.existsSync)(planPath) && existingPlanPath !== planPath) {
5579
5970
  try {
5580
- const translatedPlan = JSON.parse(await (0, import_promises14.readFile)(planPath, "utf-8"));
5971
+ const translatedPlan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
5581
5972
  if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
5582
5973
  console.log();
5583
- console.log(import_chalk12.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
5974
+ console.log(import_chalk16.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
5584
5975
  const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
5585
5976
  if (useTranslated) {
5586
5977
  plan = translatedPlan;
@@ -5592,7 +5983,7 @@ async function runPlanPhase(params) {
5592
5983
  }
5593
5984
  if (!useExistingTranslation) {
5594
5985
  console.log();
5595
- console.log(import_chalk12.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
5986
+ console.log(import_chalk16.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
5596
5987
  const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
5597
5988
  if (translate) await doTranslate("Translation failed, using original");
5598
5989
  }
@@ -5601,8 +5992,8 @@ async function runPlanPhase(params) {
5601
5992
  }
5602
5993
 
5603
5994
  // src/commands/phases/validation-phase.ts
5604
- var import_promises15 = require("fs/promises");
5605
- var import_chalk13 = __toESM(require("chalk"));
5995
+ var import_promises16 = require("fs/promises");
5996
+ var import_chalk17 = __toESM(require("chalk"));
5606
5997
  async function runValidationPhase(params) {
5607
5998
  let { plan } = params;
5608
5999
  const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
@@ -5653,21 +6044,21 @@ async function runValidationPhase(params) {
5653
6044
  const stepNum = result.stepIndex + 1;
5654
6045
  const step = plan.steps[result.stepIndex];
5655
6046
  console.log();
5656
- console.log(import_chalk13.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
6047
+ console.log(import_chalk17.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
5657
6048
  const caption = step.caption || "";
5658
- if (caption) console.log(import_chalk13.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
5659
- console.log(import_chalk13.default.dim(` Target: ${step.target || "none"}`));
5660
- if (step.value) console.log(import_chalk13.default.dim(` Value: ${step.value}`));
5661
- console.log(import_chalk13.default.dim(` Error: ${result.error}`));
6049
+ if (caption) console.log(import_chalk17.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
6050
+ console.log(import_chalk17.default.dim(` Target: ${step.target || "none"}`));
6051
+ if (step.value) console.log(import_chalk17.default.dim(` Value: ${step.value}`));
6052
+ console.log(import_chalk17.default.dim(` Error: ${result.error}`));
5662
6053
  if (result.suggestion) {
5663
- console.log(import_chalk13.default.yellow(` Hint: ${result.suggestion}`));
6054
+ console.log(import_chalk17.default.yellow(` Hint: ${result.suggestion}`));
5664
6055
  }
5665
6056
  console.log();
5666
6057
  const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
5667
6058
  if (isUploadMissingFile) {
5668
6059
  const filePath = await promptInput('Enter file path to upload (or "skip"): ');
5669
6060
  if (filePath.toLowerCase() === "skip") {
5670
- console.log(import_chalk13.default.yellow(` Skipping step ${stepNum}`));
6061
+ console.log(import_chalk17.default.yellow(` Skipping step ${stepNum}`));
5671
6062
  continue;
5672
6063
  }
5673
6064
  if (filePath) {
@@ -5676,13 +6067,13 @@ async function runValidationPhase(params) {
5676
6067
  const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
5677
6068
  const newResult2 = revalidation2.results[result.stepIndex];
5678
6069
  if (newResult2?.success) {
5679
- console.log(import_chalk13.default.green(` \u2713 Step ${stepNum} now passes`));
6070
+ console.log(import_chalk17.default.green(` \u2713 Step ${stepNum} now passes`));
5680
6071
  validationResult = revalidation2;
5681
6072
  continue;
5682
6073
  }
5683
6074
  result.error = newResult2?.error;
5684
6075
  result.suggestion = newResult2?.suggestion;
5685
- console.log(import_chalk13.default.red(` Still failing: ${result.error}`));
6076
+ console.log(import_chalk17.default.red(` Still failing: ${result.error}`));
5686
6077
  }
5687
6078
  }
5688
6079
  const fixTask = task(`Fixing step ${stepNum} with AI`);
@@ -5704,8 +6095,8 @@ async function runValidationPhase(params) {
5704
6095
  await savePlan(plan, planPath);
5705
6096
  fixTask.succeed("AI suggested a fix");
5706
6097
  const fixedStep = plan.steps[result.stepIndex];
5707
- console.log(import_chalk13.default.dim(` New target: ${fixedStep.target || "none"}`));
5708
- if (fixedStep.value) console.log(import_chalk13.default.dim(` New value: ${fixedStep.value}`));
6098
+ console.log(import_chalk17.default.dim(` New target: ${fixedStep.target || "none"}`));
6099
+ if (fixedStep.value) console.log(import_chalk17.default.dim(` New value: ${fixedStep.value}`));
5709
6100
  } catch {
5710
6101
  fixTask.fail("AI could not fix automatically");
5711
6102
  }
@@ -5721,14 +6112,14 @@ async function runValidationPhase(params) {
5721
6112
  result.error = newResult?.error;
5722
6113
  result.suggestion = newResult?.suggestion;
5723
6114
  console.log();
5724
- console.log(import_chalk13.default.yellow(` Edit step ${stepNum} in: `) + import_chalk13.default.cyan(formatClickablePath(planPath)));
6115
+ console.log(import_chalk17.default.yellow(` Edit step ${stepNum} in: `) + import_chalk17.default.cyan(formatClickablePath(planPath)));
5725
6116
  console.log();
5726
6117
  const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
5727
6118
  if (choice.toLowerCase() === "skip") {
5728
- console.log(import_chalk13.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
6119
+ console.log(import_chalk17.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
5729
6120
  continue;
5730
6121
  }
5731
- plan = JSON.parse(await (0, import_promises15.readFile)(planPath, "utf-8"));
6122
+ plan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
5732
6123
  const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
5733
6124
  const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
5734
6125
  const finalResult = finalRevalidation.results[result.stepIndex];
@@ -5752,31 +6143,31 @@ async function runValidationPhase(params) {
5752
6143
  }
5753
6144
 
5754
6145
  // src/commands/utils/chrome-extension.ts
5755
- var import_node_fs15 = require("fs");
5756
- var import_node_os2 = require("os");
5757
- var import_node_path22 = require("path");
6146
+ var import_node_fs16 = require("fs");
6147
+ var import_node_os3 = require("os");
6148
+ var import_node_path23 = require("path");
5758
6149
  function resolveChromeExtension(extensionId) {
5759
6150
  const platform = process.platform;
5760
6151
  const possibleDirs = [];
5761
6152
  const getChromeExtDirs = (userDataPath) => {
5762
6153
  const dirs = [];
5763
6154
  try {
5764
- const profiles = (0, import_node_fs15.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
6155
+ const profiles = (0, import_node_fs16.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
5765
6156
  for (const profile of profiles) {
5766
- dirs.push((0, import_node_path22.join)(userDataPath, profile, "Extensions"));
6157
+ dirs.push((0, import_node_path23.join)(userDataPath, profile, "Extensions"));
5767
6158
  }
5768
6159
  } catch {
5769
- dirs.push((0, import_node_path22.join)(userDataPath, "Default", "Extensions"));
6160
+ dirs.push((0, import_node_path23.join)(userDataPath, "Default", "Extensions"));
5770
6161
  }
5771
6162
  return dirs;
5772
6163
  };
5773
6164
  if (platform === "win32") {
5774
- possibleDirs.push(...getChromeExtDirs((0, import_node_path22.join)((0, import_node_os2.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
6165
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
5775
6166
  } else if (platform === "darwin") {
5776
- possibleDirs.push(...getChromeExtDirs((0, import_node_path22.join)((0, import_node_os2.homedir)(), "Library", "Application Support", "Google", "Chrome")));
6167
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
5777
6168
  } else {
5778
6169
  try {
5779
- const windowsUsers = (0, import_node_fs15.readdirSync)("/mnt/c/Users").filter(
6170
+ const windowsUsers = (0, import_node_fs16.readdirSync)("/mnt/c/Users").filter(
5780
6171
  (u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
5781
6172
  );
5782
6173
  for (const user of windowsUsers) {
@@ -5784,22 +6175,22 @@ function resolveChromeExtension(extensionId) {
5784
6175
  }
5785
6176
  } catch {
5786
6177
  }
5787
- possibleDirs.push(...getChromeExtDirs((0, import_node_path22.join)((0, import_node_os2.homedir)(), ".config", "google-chrome")));
6178
+ possibleDirs.push(...getChromeExtDirs((0, import_node_path23.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
5788
6179
  }
5789
6180
  for (const chromeExtDir of possibleDirs) {
5790
- const extDir = (0, import_node_path22.join)(chromeExtDir, extensionId);
5791
- if (!(0, import_node_fs15.existsSync)(extDir)) continue;
6181
+ const extDir = (0, import_node_path23.join)(chromeExtDir, extensionId);
6182
+ if (!(0, import_node_fs16.existsSync)(extDir)) continue;
5792
6183
  try {
5793
- const versions = (0, import_node_fs15.readdirSync)(extDir).filter((f) => !f.startsWith("."));
6184
+ const versions = (0, import_node_fs16.readdirSync)(extDir).filter((f) => !f.startsWith("."));
5794
6185
  if (versions.length === 0) continue;
5795
6186
  const latestVersion = versions.sort().pop();
5796
- const extPath = (0, import_node_path22.join)(extDir, latestVersion);
6187
+ const extPath = (0, import_node_path23.join)(extDir, latestVersion);
5797
6188
  let name = extensionId;
5798
6189
  let popupPath = "popup.html";
5799
- const manifestPath = (0, import_node_path22.join)(extPath, "manifest.json");
5800
- if ((0, import_node_fs15.existsSync)(manifestPath)) {
6190
+ const manifestPath = (0, import_node_path23.join)(extPath, "manifest.json");
6191
+ if ((0, import_node_fs16.existsSync)(manifestPath)) {
5801
6192
  try {
5802
- const manifest = JSON.parse((0, import_node_fs15.readFileSync)(manifestPath, "utf-8"));
6193
+ const manifest = JSON.parse((0, import_node_fs16.readFileSync)(manifestPath, "utf-8"));
5803
6194
  if (manifest.name && !manifest.name.startsWith("__MSG_")) {
5804
6195
  name = manifest.name;
5805
6196
  }
@@ -5819,27 +6210,27 @@ function resolveChromeExtension(extensionId) {
5819
6210
  }
5820
6211
 
5821
6212
  // src/commands/record-cmd.ts
5822
- async function recordCommand(url, langOverride, description, skipVoice, fresh, autoYes2) {
6213
+ async function recordCommand(url, options = {}) {
6214
+ const { langOverride, description, skipVoice, fresh, autoYes: autoYes2, rethrow } = options;
5823
6215
  if (autoYes2) setAutoYes(true);
5824
6216
  console.log();
5825
6217
  showLogo();
5826
- checkCredentials();
5827
6218
  const { config, created: configCreated } = await loadConfig(url, langOverride);
5828
6219
  let chromeExtension = null;
5829
6220
  if (url.startsWith("chrome:")) {
5830
6221
  const extensionId = url.slice(7);
5831
6222
  chromeExtension = resolveChromeExtension(extensionId);
5832
6223
  if (!chromeExtension) {
5833
- console.log(import_chalk14.default.red(`
6224
+ console.log(import_chalk18.default.red(`
5834
6225
  \u2717 Chrome extension not found: ${extensionId}`));
5835
- console.log(import_chalk14.default.dim(` Make sure the extension is installed in Chrome
6226
+ console.log(import_chalk18.default.dim(` Make sure the extension is installed in Chrome
5836
6227
  `));
5837
6228
  process.exit(1);
5838
6229
  }
5839
6230
  config.browser = { extension: chromeExtension.path };
5840
6231
  url = chromeExtension.popupUrl;
5841
- console.log(import_chalk14.default.dim(` Extension: ${chromeExtension.name}`));
5842
- console.log(import_chalk14.default.dim(` Path: ${chromeExtension.path}`));
6232
+ console.log(import_chalk18.default.dim(` Extension: ${chromeExtension.name}`));
6233
+ console.log(import_chalk18.default.dim(` Path: ${chromeExtension.path}`));
5843
6234
  }
5844
6235
  config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
5845
6236
  const userData = await loadUserData();
@@ -5852,20 +6243,20 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
5852
6243
  if (!authResult) return;
5853
6244
  const { userEmail, userTier, localVideoCount } = authResult;
5854
6245
  if (configCreated) {
5855
- const configPath = (0, import_node_path23.join)(process.cwd(), CONFIG_FILE);
6246
+ const configPath = (0, import_node_path24.join)(process.cwd(), CONFIG_FILE);
5856
6247
  console.log();
5857
- console.log(import_chalk14.default.green(` \u2714 Created ${CONFIG_FILE}`));
5858
- console.log(import_chalk14.default.dim(` ${configPath}`));
6248
+ console.log(import_chalk18.default.green(` \u2714 Created ${CONFIG_FILE}`));
6249
+ console.log(import_chalk18.default.dim(` ${configPath}`));
5859
6250
  console.log();
5860
- console.log(import_chalk14.default.cyan(" Review the config file to customize:"));
5861
- console.log(import_chalk14.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
5862
- console.log(import_chalk14.default.dim(" \u2022 video: Resolution, quality, effects"));
5863
- console.log(import_chalk14.default.dim(" \u2022 music: Background music path and volume"));
5864
- console.log(import_chalk14.default.dim(" \u2022 output: Output directory"));
6251
+ console.log(import_chalk18.default.cyan(" Review the config file to customize:"));
6252
+ console.log(import_chalk18.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
6253
+ console.log(import_chalk18.default.dim(" \u2022 video: Resolution, quality, effects"));
6254
+ console.log(import_chalk18.default.dim(" \u2022 music: Background music path and volume"));
6255
+ console.log(import_chalk18.default.dim(" \u2022 output: Output directory"));
5865
6256
  console.log();
5866
6257
  const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
5867
6258
  if (!proceed) {
5868
- console.log(import_chalk14.default.yellow(`
6259
+ console.log(import_chalk18.default.yellow(`
5869
6260
  Edit ${CONFIG_FILE} and run again.
5870
6261
  `));
5871
6262
  process.exit(0);
@@ -5873,17 +6264,17 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
5873
6264
  }
5874
6265
  if (langOverride) config.lang = langOverride;
5875
6266
  if (config.music?.path) {
5876
- const musicPath = (0, import_node_path23.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path23.resolve)(config.output.dir, config.music.path);
5877
- if (!(0, import_node_fs16.existsSync)(musicPath)) {
6267
+ const musicPath = (0, import_node_path24.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path24.resolve)(config.output.dir, config.music.path);
6268
+ if (!(0, import_node_fs17.existsSync)(musicPath)) {
5878
6269
  console.log();
5879
- console.log(import_chalk14.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
5880
- console.log(import_chalk14.default.dim(` Config: ${(0, import_node_path23.join)(process.cwd(), CONFIG_FILE)}`));
6270
+ console.log(import_chalk18.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
6271
+ console.log(import_chalk18.default.dim(` Config: ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}`));
5881
6272
  console.log();
5882
6273
  const proceed = await confirm(`${INDENT3}Continue without background music?`);
5883
6274
  if (!proceed) {
5884
- console.log(import_chalk14.default.yellow(`
6275
+ console.log(import_chalk18.default.yellow(`
5885
6276
  Edit music.path in config and run again.`));
5886
- console.log(import_chalk14.default.dim(` ${(0, import_node_path23.join)(process.cwd(), CONFIG_FILE)}
6277
+ console.log(import_chalk18.default.dim(` ${(0, import_node_path24.join)(process.cwd(), CONFIG_FILE)}
5887
6278
  `));
5888
6279
  process.exit(0);
5889
6280
  }
@@ -5893,10 +6284,10 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
5893
6284
  const captions = await loadCaptions();
5894
6285
  const workDir = await prepareWorkDir();
5895
6286
  const targetLang = config.lang || "en";
5896
- const planPath = (0, import_node_path23.join)(config.output.dir, `plan-${targetLang}.json`);
5897
- const legacyPlanPath = (0, import_node_path23.join)(config.output.dir, "plan.json");
6287
+ const planPath = (0, import_node_path24.join)(config.output.dir, `plan-${targetLang}.json`);
6288
+ const legacyPlanPath = (0, import_node_path24.join)(config.output.dir, "plan.json");
5898
6289
  const isExtensionUrl = url.startsWith("chrome-extension://");
5899
- await (0, import_promises16.mkdir)(config.output.dir, { recursive: true });
6290
+ await (0, import_promises17.mkdir)(config.output.dir, { recursive: true });
5900
6291
  await setLogFilePath(config.output.dir);
5901
6292
  try {
5902
6293
  const plan = await runPlanPhase({
@@ -5932,35 +6323,37 @@ async function recordCommand(url, langOverride, description, skipVoice, fresh, a
5932
6323
  await saveUserData({ localVideoCount: newCount });
5933
6324
  const remaining = FREE_VIDEO_LIMIT - newCount;
5934
6325
  if (remaining > 0) {
5935
- console.log(import_chalk14.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
6326
+ console.log(import_chalk18.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
5936
6327
  }
5937
6328
  }
5938
6329
  console.log();
5939
- console.log(import_chalk14.default.bold.green(" \u2713 Done!"));
6330
+ console.log(import_chalk18.default.bold.green(" \u2713 Done!"));
5940
6331
  console.log();
5941
- console.log(import_chalk14.default.cyan(` ${formatClickablePath(finalPath)}`));
6332
+ console.log(import_chalk18.default.cyan(` ${formatClickablePath(finalPath)}`));
5942
6333
  console.log();
6334
+ return finalPath;
5943
6335
  } catch (err) {
5944
6336
  await logError("Record Command", err);
6337
+ if (rethrow) throw err;
5945
6338
  console.log();
5946
- console.log(import_chalk14.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
5947
- console.log(import_chalk14.default.dim(` See ${logFilePath} for details`));
6339
+ console.log(import_chalk18.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
6340
+ console.log(import_chalk18.default.dim(` See ${logFilePath} for details`));
5948
6341
  process.exit(1);
5949
6342
  }
5950
6343
  }
5951
6344
 
5952
6345
  // src/commands/script-cmd.ts
5953
- var import_promises17 = require("fs/promises");
5954
- var import_node_path24 = require("path");
5955
- var import_chalk15 = __toESM(require("chalk"));
6346
+ var import_promises18 = require("fs/promises");
6347
+ var import_node_path25 = require("path");
6348
+ var import_chalk19 = __toESM(require("chalk"));
5956
6349
  init_lib();
5957
6350
  async function scriptCommand(url, scriptPath, lang) {
5958
6351
  const { config, workDir, captions } = await initCommandEnv(url, lang);
5959
6352
  header("Script");
5960
- const raw = await (0, import_promises17.readFile)(scriptPath, "utf-8");
6353
+ const raw = await (0, import_promises18.readFile)(scriptPath, "utf-8");
5961
6354
  const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
5962
6355
  if (lines.length === 0) {
5963
- console.log(import_chalk15.default.red(" Script file is empty or has no non-comment lines"));
6356
+ console.log(import_chalk19.default.red(" Script file is empty or has no non-comment lines"));
5964
6357
  process.exit(1);
5965
6358
  }
5966
6359
  info(`${lines.length} captions from script`);
@@ -5972,7 +6365,7 @@ async function scriptCommand(url, scriptPath, lang) {
5972
6365
  const planTask = task("Mapping script to UI actions");
5973
6366
  const appTitle = new URL(url).hostname.replace(/^www\./, "");
5974
6367
  const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
5975
- const planPath = (0, import_node_path24.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
6368
+ const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
5976
6369
  await savePlan(plan, planPath);
5977
6370
  planTask.succeed(`${plan.steps.length} steps planned`);
5978
6371
  header("Voiceover");
@@ -5992,15 +6385,15 @@ async function scriptCommand(url, scriptPath, lang) {
5992
6385
  const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
5993
6386
  const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, "free");
5994
6387
  console.log();
5995
- console.log(import_chalk15.default.bold.green(" Done!"));
5996
- console.log(import_chalk15.default.cyan(` ${finalPath}`));
6388
+ console.log(import_chalk19.default.bold.green(" Done!"));
6389
+ console.log(import_chalk19.default.cyan(` ${finalPath}`));
5997
6390
  }
5998
6391
 
5999
6392
  // src/qp.ts
6000
6393
  init_lib();
6001
6394
  import_dotenv.default.config({ path: ".env.local", quiet: true });
6002
6395
  import_dotenv.default.config({ quiet: true });
6003
- function normalizeUrl2(input) {
6396
+ function normalizeUrl3(input) {
6004
6397
  if (input.startsWith("chrome-extension://") || input.startsWith("chrome:")) {
6005
6398
  return input;
6006
6399
  }
@@ -6032,6 +6425,22 @@ function parseArgs(args) {
6032
6425
  return { ...base, command: "help" };
6033
6426
  }
6034
6427
  const first = args[0];
6428
+ if (first === "login") {
6429
+ return { ...base, command: "login" };
6430
+ }
6431
+ if (first === "logout") {
6432
+ return { ...base, command: "logout" };
6433
+ }
6434
+ if (first === "auth") {
6435
+ const sub = args[1];
6436
+ if (sub === "list" || sub === void 0) {
6437
+ return { ...base, command: "auth-list" };
6438
+ }
6439
+ if (sub === "clear") {
6440
+ return { ...base, command: "auth-clear", url: args[2] ? normalizeUrl3(args[2]) : void 0 };
6441
+ }
6442
+ return { ...base, command: "auth", url: normalizeUrl3(sub) };
6443
+ }
6035
6444
  if (first === "init") {
6036
6445
  return { ...base, command: "init" };
6037
6446
  }
@@ -6055,7 +6464,7 @@ function parseArgs(args) {
6055
6464
  if (arg2.startsWith("lang=")) {
6056
6465
  lang2 = arg2.slice(5);
6057
6466
  } else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
6058
- url2 = normalizeUrl2(arg2);
6467
+ url2 = normalizeUrl3(arg2);
6059
6468
  }
6060
6469
  }
6061
6470
  if (!url2) return { ...base, command: "help" };
@@ -6088,7 +6497,7 @@ function parseArgs(args) {
6088
6497
  if (arg2.endsWith(".js") || arg2.endsWith(".ts") || arg2.startsWith("./") || arg2.startsWith("/")) {
6089
6498
  continue;
6090
6499
  }
6091
- url = normalizeUrl2(arg2);
6500
+ url = normalizeUrl3(arg2);
6092
6501
  }
6093
6502
  }
6094
6503
  if (!url) {
@@ -6104,7 +6513,27 @@ async function main() {
6104
6513
  console.log(`quickpeek v${VERSION}`);
6105
6514
  break;
6106
6515
  case "help":
6107
- showHelp();
6516
+ await showHelp();
6517
+ break;
6518
+ case "login":
6519
+ await loginCommand();
6520
+ break;
6521
+ case "logout":
6522
+ await logoutCommand();
6523
+ break;
6524
+ case "auth":
6525
+ if (!parsed.url) {
6526
+ console.log(import_chalk20.default.red(" Missing URL"));
6527
+ console.log(import_chalk20.default.dim(" Run: qp auth <url>"));
6528
+ process.exit(1);
6529
+ }
6530
+ await authCommand(parsed.url);
6531
+ break;
6532
+ case "auth-clear":
6533
+ await authClearCommand(parsed.url);
6534
+ break;
6535
+ case "auth-list":
6536
+ await authListCommand();
6108
6537
  break;
6109
6538
  case "init":
6110
6539
  await initCommand();
@@ -6114,8 +6543,8 @@ async function main() {
6114
6543
  break;
6115
6544
  case "record":
6116
6545
  if (!parsed.url) {
6117
- console.log(import_chalk16.default.red(" Missing URL"));
6118
- console.log(import_chalk16.default.dim(" Run: qp --help"));
6546
+ console.log(import_chalk20.default.red(" Missing URL"));
6547
+ console.log(import_chalk20.default.dim(" Run: qp --help"));
6119
6548
  process.exit(1);
6120
6549
  }
6121
6550
  setAutoYes(parsed.yes);
@@ -6124,13 +6553,18 @@ async function main() {
6124
6553
  showLogo();
6125
6554
  await scriptCommand(parsed.url, parsed.scriptPath, parsed.lang);
6126
6555
  } else {
6127
- await recordCommand(parsed.url, parsed.lang, parsed.description, parsed.skipVoice, parsed.fresh);
6556
+ await recordCommand(parsed.url, {
6557
+ langOverride: parsed.lang,
6558
+ description: parsed.description,
6559
+ skipVoice: parsed.skipVoice,
6560
+ fresh: parsed.fresh
6561
+ });
6128
6562
  }
6129
6563
  break;
6130
6564
  case "capture":
6131
6565
  if (!parsed.url) {
6132
- console.log(import_chalk16.default.red(" Missing URL"));
6133
- console.log(import_chalk16.default.dim(" Run: qp --help"));
6566
+ console.log(import_chalk20.default.red(" Missing URL"));
6567
+ console.log(import_chalk20.default.dim(" Run: qp --help"));
6134
6568
  process.exit(1);
6135
6569
  }
6136
6570
  console.log();