@spark-apps/quickpeek 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -204
- package/dist/highlight.css +15 -15
- package/dist/index.d.mts +79 -3
- package/dist/index.mjs +284 -65
- package/dist/mcp-tools.mjs +1861 -872
- package/dist/qp.js +1606 -838
- package/dist/{scraping-BK8pIJhm.d.mts → scraping-BgepklP3.d.mts} +51 -6
- package/dist/web.d.mts +42 -2
- package/dist/web.mjs +174 -37
- package/mcp.mjs +28 -1
- package/package.json +1 -1
package/dist/mcp-tools.mjs
CHANGED
|
@@ -43,7 +43,7 @@ function getErrorMessage(err) {
|
|
|
43
43
|
return err instanceof Error ? err.message : String(err);
|
|
44
44
|
}
|
|
45
45
|
function toTitleCase(text) {
|
|
46
|
-
return text.replace(/\w
|
|
46
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
47
47
|
if (index === 0) {
|
|
48
48
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
49
49
|
}
|
|
@@ -107,6 +107,23 @@ var init_utils = __esm({
|
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
// src/lib/api-client.ts
|
|
110
|
+
function apiKey() {
|
|
111
|
+
return process.env.SPARK_AI_KEY || "";
|
|
112
|
+
}
|
|
113
|
+
function hostOf2(url) {
|
|
114
|
+
try {
|
|
115
|
+
return new URL(url).host;
|
|
116
|
+
} catch {
|
|
117
|
+
return "AI service";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function firstLine(text) {
|
|
121
|
+
return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
|
|
122
|
+
}
|
|
123
|
+
function truncate(text, max) {
|
|
124
|
+
const trimmed = (text ?? "").trim();
|
|
125
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
|
|
126
|
+
}
|
|
110
127
|
async function getOrRefreshDirectToken() {
|
|
111
128
|
const now = Date.now();
|
|
112
129
|
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
@@ -115,7 +132,7 @@ async function getOrRefreshDirectToken() {
|
|
|
115
132
|
try {
|
|
116
133
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
117
134
|
method: "POST",
|
|
118
|
-
headers: { "X-Api-Key":
|
|
135
|
+
headers: { "X-Api-Key": apiKey() }
|
|
119
136
|
});
|
|
120
137
|
if (!response.ok) {
|
|
121
138
|
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
@@ -158,8 +175,10 @@ async function getOrRefreshRelayToken(email) {
|
|
|
158
175
|
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
|
-
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
162
|
-
|
|
178
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
|
|
179
|
+
const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
|
|
180
|
+
let lastRetriableError = null;
|
|
181
|
+
const host = hostOf2(chatUrl);
|
|
163
182
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
164
183
|
try {
|
|
165
184
|
const response = await fetch(chatUrl, {
|
|
@@ -175,36 +194,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
175
194
|
]
|
|
176
195
|
})
|
|
177
196
|
});
|
|
197
|
+
const bodyText = await response.text().catch(() => "");
|
|
178
198
|
let data = {};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
199
|
+
try {
|
|
200
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
201
|
+
} catch {
|
|
182
202
|
}
|
|
183
203
|
if (!response.ok) {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
|
|
205
|
+
const error = `${host} ${response.status}: ${upstream}`;
|
|
206
|
+
const detail = truncate(bodyText, 800);
|
|
207
|
+
const isRetriable = response.status === 429 || response.status >= 500;
|
|
208
|
+
if (isRetriable && attempt < 3) {
|
|
209
|
+
const retryAfter2 = response.headers.get("Retry-After");
|
|
210
|
+
const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
|
|
211
|
+
onRetry?.({ attempt, status: response.status, delayMs, host });
|
|
212
|
+
lastRetriableError = {
|
|
189
213
|
success: false,
|
|
190
214
|
error,
|
|
191
|
-
isRateLimit:
|
|
192
|
-
|
|
215
|
+
isRateLimit: response.status === 429,
|
|
216
|
+
status: response.status,
|
|
217
|
+
host,
|
|
218
|
+
...detail ? { detail } : {},
|
|
219
|
+
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
193
220
|
};
|
|
194
221
|
await new Promise((resolve10) => setTimeout(resolve10, Math.min(delayMs, 3e4)));
|
|
195
222
|
continue;
|
|
196
223
|
}
|
|
224
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
225
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
197
226
|
return {
|
|
198
227
|
success: false,
|
|
199
228
|
error,
|
|
200
229
|
isRateLimit: response.status === 429,
|
|
201
|
-
isAuthError: response.status === 401 || response.status === 403
|
|
230
|
+
isAuthError: response.status === 401 || response.status === 403,
|
|
231
|
+
status: response.status,
|
|
232
|
+
host,
|
|
233
|
+
...detail ? { detail } : {},
|
|
234
|
+
...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
|
|
202
235
|
};
|
|
203
236
|
}
|
|
204
237
|
if (!data.success || !data.response) {
|
|
205
238
|
return {
|
|
206
239
|
success: false,
|
|
207
|
-
error: data.error || "
|
|
240
|
+
error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
|
|
241
|
+
status: response.status,
|
|
242
|
+
host,
|
|
243
|
+
...bodyText ? { detail: truncate(bodyText, 800) } : {}
|
|
208
244
|
};
|
|
209
245
|
}
|
|
210
246
|
return {
|
|
@@ -218,15 +254,17 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
218
254
|
}
|
|
219
255
|
};
|
|
220
256
|
} catch (err) {
|
|
221
|
-
if (attempt === 3)
|
|
257
|
+
if (attempt === 3) {
|
|
258
|
+
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
259
|
+
}
|
|
222
260
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
223
261
|
await new Promise((resolve10) => setTimeout(resolve10, delayMs));
|
|
224
262
|
}
|
|
225
263
|
}
|
|
226
|
-
return
|
|
264
|
+
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
227
265
|
}
|
|
228
|
-
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
229
|
-
if (!
|
|
266
|
+
async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
267
|
+
if (!apiKey()) {
|
|
230
268
|
return {
|
|
231
269
|
success: false,
|
|
232
270
|
error: "AI service is not configured. Please report this issue.",
|
|
@@ -235,12 +273,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
|
235
273
|
}
|
|
236
274
|
try {
|
|
237
275
|
const token = await getOrRefreshDirectToken();
|
|
238
|
-
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
276
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
|
|
239
277
|
} catch (err) {
|
|
240
278
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
241
279
|
}
|
|
242
280
|
}
|
|
243
|
-
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
281
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
|
|
244
282
|
if (!email) {
|
|
245
283
|
return {
|
|
246
284
|
success: false,
|
|
@@ -250,11 +288,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
|
250
288
|
}
|
|
251
289
|
try {
|
|
252
290
|
let token = await getOrRefreshRelayToken(email);
|
|
253
|
-
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
291
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
254
292
|
if (!result.success && result.isAuthError) {
|
|
255
293
|
cachedRelayToken = null;
|
|
256
294
|
token = await getOrRefreshRelayToken(email);
|
|
257
|
-
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
295
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
258
296
|
}
|
|
259
297
|
return result;
|
|
260
298
|
} catch (err) {
|
|
@@ -286,18 +324,19 @@ function normalizeUrl(input) {
|
|
|
286
324
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
287
325
|
return `https://${url}`;
|
|
288
326
|
}
|
|
289
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL,
|
|
327
|
+
var SERVER_BACKOFF_MS, CLI_BACKOFF_MS, SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
|
|
290
328
|
var init_api_client = __esm({
|
|
291
329
|
"src/lib/api-client.ts"() {
|
|
292
330
|
"use strict";
|
|
293
331
|
init_utils();
|
|
332
|
+
SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
333
|
+
CLI_BACKOFF_MS = [1e4, 3e4];
|
|
294
334
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
295
335
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
296
336
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
297
337
|
RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
298
338
|
RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
299
339
|
RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
300
|
-
API_KEY = process.env.SPARK_AI_KEY || "";
|
|
301
340
|
cachedDirectToken = null;
|
|
302
341
|
cachedRelayToken = null;
|
|
303
342
|
}
|
|
@@ -322,7 +361,7 @@ async function getStatusCached(email) {
|
|
|
322
361
|
function getTier(d) {
|
|
323
362
|
if (!d.subscription || !d.registered) return "free";
|
|
324
363
|
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
325
|
-
return TIERS[d.subscription.price_id] ?? "
|
|
364
|
+
return TIERS[d.subscription.price_id] ?? "pro";
|
|
326
365
|
}
|
|
327
366
|
async function getTierByEmail(email) {
|
|
328
367
|
try {
|
|
@@ -339,17 +378,18 @@ async function isVerified(email) {
|
|
|
339
378
|
}
|
|
340
379
|
}
|
|
341
380
|
function pricingUrl(opts) {
|
|
342
|
-
const u = new URL(`${BASE}
|
|
381
|
+
const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
|
|
343
382
|
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
344
383
|
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
345
384
|
return u.toString();
|
|
346
385
|
}
|
|
347
|
-
var APP_ID, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
386
|
+
var APP_ID, OWNER_SLUG, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
348
387
|
var init_SparkStripe_feature_gating = __esm({
|
|
349
388
|
"src/lib/SparkStripe-feature-gating.ts"() {
|
|
350
389
|
"use strict";
|
|
351
390
|
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
352
|
-
|
|
391
|
+
OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
|
|
392
|
+
BASE = "https://sparkpay.dev";
|
|
353
393
|
TIERS = {
|
|
354
394
|
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
355
395
|
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
@@ -401,7 +441,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
|
|
|
401
441
|
var init_config = __esm({
|
|
402
442
|
"src/lib/config.ts"() {
|
|
403
443
|
"use strict";
|
|
404
|
-
VERSION = "1.
|
|
444
|
+
VERSION = "1.2.1";
|
|
405
445
|
CONFIG_FILE = "quickpeek.config.json";
|
|
406
446
|
DEFAULT_CONFIG = {
|
|
407
447
|
plan: { maxSteps: 8 },
|
|
@@ -551,7 +591,10 @@ var init_scraping = __esm({
|
|
|
551
591
|
});
|
|
552
592
|
|
|
553
593
|
// src/lib/crawl.ts
|
|
554
|
-
import { chromium } from "playwright";
|
|
594
|
+
import { chromium as chromium3 } from "playwright";
|
|
595
|
+
function idSelector(id) {
|
|
596
|
+
return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
|
|
597
|
+
}
|
|
555
598
|
function getActionForType(type) {
|
|
556
599
|
switch (type) {
|
|
557
600
|
case "button":
|
|
@@ -569,20 +612,29 @@ function getActionForType(type) {
|
|
|
569
612
|
return "type";
|
|
570
613
|
}
|
|
571
614
|
}
|
|
572
|
-
async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
573
|
-
const
|
|
615
|
+
async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
616
|
+
const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
|
|
617
|
+
const cached = crawlCache.get(cacheKey);
|
|
574
618
|
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
575
619
|
task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
576
|
-
return { title: cached.title, elements: [...cached.elements] };
|
|
620
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
577
621
|
}
|
|
578
622
|
const browserTask = task2("Launching browser");
|
|
579
|
-
const browser = await
|
|
580
|
-
const context = await browser.newContext(
|
|
623
|
+
const browser = await chromium3.launch({ headless: true });
|
|
624
|
+
const context = await browser.newContext({
|
|
625
|
+
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
626
|
+
// Crawl at the size we will record at, or the measurements below describe
|
|
627
|
+
// a page nobody will ever see.
|
|
628
|
+
...viewport ? { viewport } : {}
|
|
629
|
+
});
|
|
581
630
|
const page = await context.newPage();
|
|
582
631
|
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
583
632
|
const crawlTask = task2("Crawling page");
|
|
584
633
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
585
634
|
const title = await page.title();
|
|
635
|
+
const scrollable = await page.evaluate(
|
|
636
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
637
|
+
);
|
|
586
638
|
const rawElements = await page.evaluate(`
|
|
587
639
|
(() => {
|
|
588
640
|
const items = [];
|
|
@@ -672,9 +724,9 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
672
724
|
} else if (el.isFile || el.type === "file") {
|
|
673
725
|
selector = 'input[type="file"]';
|
|
674
726
|
} else if (el.type === "range") {
|
|
675
|
-
selector = el.id ?
|
|
727
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
676
728
|
} else if (el.type === "checkbox") {
|
|
677
|
-
selector = el.id ?
|
|
729
|
+
selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
|
|
678
730
|
} else if (el.type === "button") {
|
|
679
731
|
if (btnTexts[el.text] === 1) {
|
|
680
732
|
selector = `text="${el.text}"`;
|
|
@@ -684,7 +736,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
684
736
|
} else if (el.type === "link") {
|
|
685
737
|
selector = `text="${el.text}"`;
|
|
686
738
|
} else if (el.id) {
|
|
687
|
-
selector =
|
|
739
|
+
selector = idSelector(el.id);
|
|
688
740
|
} else if (el.name) {
|
|
689
741
|
selector = `[name="${el.name}"]`;
|
|
690
742
|
} else if (el.placeholder) {
|
|
@@ -712,12 +764,12 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
712
764
|
});
|
|
713
765
|
await browser.close();
|
|
714
766
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
715
|
-
crawlCache.set(
|
|
767
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
716
768
|
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
717
769
|
const oldest = crawlCache.keys().next().value;
|
|
718
770
|
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
719
771
|
}
|
|
720
|
-
return { title, elements };
|
|
772
|
+
return { title, elements, scrollable };
|
|
721
773
|
}
|
|
722
774
|
var noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
723
775
|
var init_crawl = __esm({
|
|
@@ -771,7 +823,7 @@ var init_watermark = __esm({
|
|
|
771
823
|
});
|
|
772
824
|
|
|
773
825
|
// src/lib/ffmpeg/spawn.ts
|
|
774
|
-
import { spawn } from "child_process";
|
|
826
|
+
import { spawn as spawn2 } from "child_process";
|
|
775
827
|
function toWSLPath(path) {
|
|
776
828
|
if (!isWindowsRuntime) return path;
|
|
777
829
|
return windowsDrivePathToWsl(path) ?? path.replace(/\\/g, "/");
|
|
@@ -785,9 +837,9 @@ function convertArgToWSL(arg2) {
|
|
|
785
837
|
function spawnFFmpeg(cmd2, args) {
|
|
786
838
|
if (isWindowsRuntime) {
|
|
787
839
|
const wslArgs = args.map(convertArgToWSL);
|
|
788
|
-
return
|
|
840
|
+
return spawn2("wsl", ["-e", cmd2, ...wslArgs]);
|
|
789
841
|
}
|
|
790
|
-
return
|
|
842
|
+
return spawn2(cmd2, args);
|
|
791
843
|
}
|
|
792
844
|
function runFFmpeg(args, errorLabel) {
|
|
793
845
|
return new Promise((resolve10, reject) => {
|
|
@@ -986,8 +1038,39 @@ function getMediaDuration(filePath) {
|
|
|
986
1038
|
proc.on("error", reject);
|
|
987
1039
|
});
|
|
988
1040
|
}
|
|
1041
|
+
function getAudioCodec(filePath) {
|
|
1042
|
+
return new Promise((resolve10) => {
|
|
1043
|
+
const args = [
|
|
1044
|
+
"-v",
|
|
1045
|
+
"error",
|
|
1046
|
+
"-select_streams",
|
|
1047
|
+
"a:0",
|
|
1048
|
+
"-show_entries",
|
|
1049
|
+
"stream=codec_name",
|
|
1050
|
+
"-of",
|
|
1051
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
1052
|
+
filePath
|
|
1053
|
+
];
|
|
1054
|
+
const proc = spawnFFmpeg("ffprobe", args);
|
|
1055
|
+
let stdout = "";
|
|
1056
|
+
proc.stdout.on("data", (data) => {
|
|
1057
|
+
stdout += data;
|
|
1058
|
+
});
|
|
1059
|
+
proc.on("close", (code) => resolve10(code === 0 ? stdout.trim() : ""));
|
|
1060
|
+
proc.on("error", () => resolve10(""));
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async function isHonestWav(filePath) {
|
|
1064
|
+
const codec = await getAudioCodec(filePath);
|
|
1065
|
+
return codec.startsWith("pcm_");
|
|
1066
|
+
}
|
|
1067
|
+
function buildConcatArgs(listPath, output) {
|
|
1068
|
+
const isWav = output.toLowerCase().endsWith(".wav");
|
|
1069
|
+
const codec = isWav ? ["-c:a", "pcm_s16le"] : ["-c", "copy"];
|
|
1070
|
+
return ["-y", "-f", "concat", "-safe", "0", "-i", listPath, ...codec, output];
|
|
1071
|
+
}
|
|
989
1072
|
function concatMedia(listPath, output) {
|
|
990
|
-
return runFFmpeg(
|
|
1073
|
+
return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
|
|
991
1074
|
}
|
|
992
1075
|
function generateSilence(outputPath, durationMs) {
|
|
993
1076
|
return runFFmpeg(
|
|
@@ -1236,6 +1319,114 @@ var init_ffmpeg2 = __esm({
|
|
|
1236
1319
|
}
|
|
1237
1320
|
});
|
|
1238
1321
|
|
|
1322
|
+
// src/lib/json-repair.ts
|
|
1323
|
+
function repairUnicodeEscapes(content) {
|
|
1324
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
1325
|
+
const code = Number.parseInt(hex, 16);
|
|
1326
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
1327
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
1328
|
+
}
|
|
1329
|
+
function salvageTruncatedJson(content) {
|
|
1330
|
+
const stack = [];
|
|
1331
|
+
const cuts = [];
|
|
1332
|
+
let inString = false;
|
|
1333
|
+
let escaped = false;
|
|
1334
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
1335
|
+
for (let i = 0; i < content.length; i++) {
|
|
1336
|
+
const ch = content[i];
|
|
1337
|
+
if (inString) {
|
|
1338
|
+
if (escaped) escaped = false;
|
|
1339
|
+
else if (ch === "\\") escaped = true;
|
|
1340
|
+
else if (ch === '"') {
|
|
1341
|
+
inString = false;
|
|
1342
|
+
record(i + 1, false);
|
|
1343
|
+
}
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
if (ch === '"') {
|
|
1347
|
+
inString = true;
|
|
1348
|
+
} else if (ch === "{" || ch === "[") {
|
|
1349
|
+
stack.push(ch);
|
|
1350
|
+
} else if (ch === "}" || ch === "]") {
|
|
1351
|
+
stack.pop();
|
|
1352
|
+
record(i + 1, true);
|
|
1353
|
+
} else if (ch === ",") {
|
|
1354
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const tryCut = (cut) => {
|
|
1358
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
1359
|
+
if (head.endsWith(":")) return null;
|
|
1360
|
+
let closed = head;
|
|
1361
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
1362
|
+
closed += CLOSERS[cut.open[d]];
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
JSON.parse(closed);
|
|
1366
|
+
return closed;
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
1372
|
+
for (const containerOnly of [true, false]) {
|
|
1373
|
+
for (let n = 0; n < attempts; n++) {
|
|
1374
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
1375
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
1376
|
+
const salvaged = tryCut(cut);
|
|
1377
|
+
if (salvaged !== null) return salvaged;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
function extractJsonCandidates(content) {
|
|
1383
|
+
let text = content.trim();
|
|
1384
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
1385
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
1386
|
+
const start = text.search(/[{[]/);
|
|
1387
|
+
if (start === -1) return [];
|
|
1388
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
1389
|
+
const end = text.lastIndexOf(closer);
|
|
1390
|
+
const candidates = [text.slice(start)];
|
|
1391
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
1392
|
+
return candidates;
|
|
1393
|
+
}
|
|
1394
|
+
function parseAIJson(content) {
|
|
1395
|
+
const candidates = extractJsonCandidates(content);
|
|
1396
|
+
if (candidates.length === 0) return null;
|
|
1397
|
+
for (const text of candidates) {
|
|
1398
|
+
try {
|
|
1399
|
+
return { value: JSON.parse(text), repaired: false };
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
for (const text of candidates) {
|
|
1404
|
+
try {
|
|
1405
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
for (const text of candidates) {
|
|
1410
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
1411
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
1412
|
+
if (salvaged === null) continue;
|
|
1413
|
+
try {
|
|
1414
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
var MAX_SALVAGE_ATTEMPTS, CLOSERS;
|
|
1422
|
+
var init_json_repair = __esm({
|
|
1423
|
+
"src/lib/json-repair.ts"() {
|
|
1424
|
+
"use strict";
|
|
1425
|
+
MAX_SALVAGE_ATTEMPTS = 500;
|
|
1426
|
+
CLOSERS = { "{": "}", "[": "]" };
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1239
1430
|
// src/lib/prompts.ts
|
|
1240
1431
|
function buildSystemPrompt(maxSteps) {
|
|
1241
1432
|
let scope;
|
|
@@ -1289,7 +1480,7 @@ EXAMPLES:
|
|
|
1289
1480
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
1290
1481
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
1291
1482
|
}
|
|
1292
|
-
function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
|
|
1483
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
1293
1484
|
const lang = langName ? `
|
|
1294
1485
|
Language: Write ALL captions in ${langName}.` : "";
|
|
1295
1486
|
const purpose = description ? `
|
|
@@ -1327,8 +1518,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
1327
1518
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
1328
1519
|
}
|
|
1329
1520
|
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
1521
|
+
const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
|
|
1522
|
+
const scrolling = cannotScroll ? `
|
|
1523
|
+
|
|
1524
|
+
SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
|
|
1525
|
+
const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
|
|
1330
1526
|
return `URL: ${url}
|
|
1331
|
-
Title: ${title}${purpose}
|
|
1527
|
+
Title: ${title}${purpose}${scrolling}
|
|
1332
1528
|
|
|
1333
1529
|
AVAILABLE ELEMENTS (grouped by action):
|
|
1334
1530
|
${elementSummary}
|
|
@@ -1338,7 +1534,7 @@ REQUIREMENTS:
|
|
|
1338
1534
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
1339
1535
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
1340
1536
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
1341
|
-
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
1537
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
|
|
1342
1538
|
|
|
1343
1539
|
Return JSON:
|
|
1344
1540
|
{
|
|
@@ -1361,10 +1557,12 @@ TARGET SYNTAX:
|
|
|
1361
1557
|
\u2022 Sliders: input[type="range"]
|
|
1362
1558
|
\u2022 Text inputs: Use selector from list above`;
|
|
1363
1559
|
}
|
|
1560
|
+
var MIN_USEFUL_SCROLL_PX;
|
|
1364
1561
|
var init_prompts = __esm({
|
|
1365
1562
|
"src/lib/prompts.ts"() {
|
|
1366
1563
|
"use strict";
|
|
1367
1564
|
init_utils();
|
|
1565
|
+
MIN_USEFUL_SCROLL_PX = 200;
|
|
1368
1566
|
}
|
|
1369
1567
|
});
|
|
1370
1568
|
|
|
@@ -1430,7 +1628,7 @@ var init_styles = __esm({
|
|
|
1430
1628
|
async function registerFreeUser(email) {
|
|
1431
1629
|
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1432
1630
|
try {
|
|
1433
|
-
const response = await fetch(`https://
|
|
1631
|
+
const response = await fetch(`https://sparkpay.dev/api/register-free`, {
|
|
1434
1632
|
method: "POST",
|
|
1435
1633
|
headers: { "Content-Type": "application/json" },
|
|
1436
1634
|
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
@@ -1465,7 +1663,7 @@ var init_subscription_client = __esm({
|
|
|
1465
1663
|
});
|
|
1466
1664
|
|
|
1467
1665
|
// src/lib/tts/helpers.ts
|
|
1468
|
-
import { existsSync as
|
|
1666
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1469
1667
|
import { readFile, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
1470
1668
|
import { isAbsolute, join as join2, resolve as resolve2 } from "path";
|
|
1471
1669
|
function toWSLPathForTTS(path) {
|
|
@@ -1500,7 +1698,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1500
1698
|
return 0;
|
|
1501
1699
|
}
|
|
1502
1700
|
const mediaPath = isAbsolute(step.value) ? step.value : resolve2(baseDir, step.value);
|
|
1503
|
-
if (!
|
|
1701
|
+
if (!existsSync4(mediaPath)) {
|
|
1504
1702
|
return 0;
|
|
1505
1703
|
}
|
|
1506
1704
|
try {
|
|
@@ -1522,7 +1720,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1522
1720
|
async function loadVoiceoverMeta(outputDir) {
|
|
1523
1721
|
const metaPath = join2(outputDir, "voiceover.json");
|
|
1524
1722
|
try {
|
|
1525
|
-
if (!
|
|
1723
|
+
if (!existsSync4(metaPath)) return null;
|
|
1526
1724
|
const content = await readFile(metaPath, "utf-8");
|
|
1527
1725
|
return JSON.parse(content);
|
|
1528
1726
|
} catch {
|
|
@@ -1561,7 +1759,7 @@ __export(timing_exports, {
|
|
|
1561
1759
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1562
1760
|
estimateWordTimings: () => estimateWordTimings
|
|
1563
1761
|
});
|
|
1564
|
-
import { existsSync as
|
|
1762
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1565
1763
|
import { join as join3 } from "path";
|
|
1566
1764
|
function estimateWordTimings(caption, duration) {
|
|
1567
1765
|
const words = caption.split(/\s+/).filter((w) => w.length > 0);
|
|
@@ -1598,9 +1796,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1598
1796
|
const audioPath = join3(audioDir, `step-${stepNum}.mp3`);
|
|
1599
1797
|
const originalPaddingPath = join3(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1600
1798
|
const silencePath = join3(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1601
|
-
if (
|
|
1799
|
+
if (existsSync5(audioPath)) {
|
|
1602
1800
|
segments.push(audioPath);
|
|
1603
|
-
if (
|
|
1801
|
+
if (existsSync5(originalPaddingPath)) {
|
|
1604
1802
|
segments.push(originalPaddingPath);
|
|
1605
1803
|
}
|
|
1606
1804
|
if (recordingDuration > voiceDuration) {
|
|
@@ -1608,7 +1806,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1608
1806
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1609
1807
|
segments.push(alignPaddingPath);
|
|
1610
1808
|
}
|
|
1611
|
-
} else if (
|
|
1809
|
+
} else if (existsSync5(silencePath)) {
|
|
1612
1810
|
if (targetDuration !== voiceDuration) {
|
|
1613
1811
|
await generateSilence(silencePath, targetDuration);
|
|
1614
1812
|
}
|
|
@@ -1696,10 +1894,25 @@ var init_voices = __esm({
|
|
|
1696
1894
|
|
|
1697
1895
|
// src/lib/tts/synthesize.ts
|
|
1698
1896
|
import { exec } from "child_process";
|
|
1699
|
-
import {
|
|
1897
|
+
import { createWriteStream } from "fs";
|
|
1898
|
+
import { stat, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
|
|
1700
1899
|
import { join as join5, resolve as resolve3 } from "path";
|
|
1900
|
+
import { pipeline } from "stream/promises";
|
|
1701
1901
|
import { promisify } from "util";
|
|
1702
1902
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1903
|
+
let pythonMsg = "";
|
|
1904
|
+
for (let attempt = 0; attempt < PYTHON_ATTEMPTS; attempt++) {
|
|
1905
|
+
try {
|
|
1906
|
+
await synthesizeWithPython(text, outputPath, voice);
|
|
1907
|
+
return;
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
pythonMsg = err instanceof Error ? err.message : String(err);
|
|
1910
|
+
if (attempt < PYTHON_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 500));
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
await synthesizeWithNode(text, outputPath, voice, pythonMsg);
|
|
1914
|
+
}
|
|
1915
|
+
async function synthesizeWithPython(text, outputPath, voice) {
|
|
1703
1916
|
const tempScriptPath = join5(resolve3(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1704
1917
|
const wslScriptPath = isWindows ? toWSLPathForTTS(resolve3(tempScriptPath)) : tempScriptPath;
|
|
1705
1918
|
const wslOutputPath = isWindows ? toWSLPathForTTS(resolve3(outputPath)) : outputPath;
|
|
@@ -1720,42 +1933,45 @@ asyncio.run(main())
|
|
|
1720
1933
|
await writeFile3(tempScriptPath, pythonScript);
|
|
1721
1934
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1722
1935
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1936
|
+
} finally {
|
|
1937
|
+
await unlink3(tempScriptPath).catch(() => {
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async function synthesizeWithNode(text, outputPath, voice, pythonMsg, maxRetries = 2) {
|
|
1942
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1943
|
+
try {
|
|
1944
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
|
|
1945
|
+
const tts = new MsEdgeTTS();
|
|
1946
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
|
|
1947
|
+
const { audioStream } = tts.toStream(text);
|
|
1948
|
+
await pipeline(audioStream, createWriteStream(outputPath));
|
|
1949
|
+
const { size } = await stat(outputPath);
|
|
1950
|
+
if (size === 0) throw new Error("No audio data received");
|
|
1951
|
+
return;
|
|
1952
|
+
} catch (nodeErr) {
|
|
1953
|
+
await unlink3(outputPath).catch(() => {
|
|
1954
|
+
});
|
|
1955
|
+
if (attempt === maxRetries) {
|
|
1956
|
+
const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
1957
|
+
throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
|
|
1744
1958
|
}
|
|
1959
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1745
1960
|
}
|
|
1746
1961
|
}
|
|
1747
1962
|
}
|
|
1748
|
-
var execAsync;
|
|
1963
|
+
var execAsync, PYTHON_ATTEMPTS;
|
|
1749
1964
|
var init_synthesize = __esm({
|
|
1750
1965
|
"src/lib/tts/synthesize.ts"() {
|
|
1751
1966
|
"use strict";
|
|
1752
1967
|
init_helpers();
|
|
1753
1968
|
execAsync = promisify(exec);
|
|
1969
|
+
PYTHON_ATTEMPTS = 2;
|
|
1754
1970
|
}
|
|
1755
1971
|
});
|
|
1756
1972
|
|
|
1757
1973
|
// src/lib/tts/generate.ts
|
|
1758
|
-
import { mkdir as
|
|
1974
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
1759
1975
|
import { join as join6 } from "path";
|
|
1760
1976
|
function estimateDurationFromText(text, wordsPerMinute = 150) {
|
|
1761
1977
|
const words = text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
@@ -1789,7 +2005,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1789
2005
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1790
2006
|
}
|
|
1791
2007
|
const audioDir = join6(workDir, "audio");
|
|
1792
|
-
await
|
|
2008
|
+
await mkdir3(audioDir, { recursive: true });
|
|
1793
2009
|
const segments = [];
|
|
1794
2010
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1795
2011
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1840,7 +2056,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1840
2056
|
return { voiceoverPath: "", stepAudio };
|
|
1841
2057
|
}
|
|
1842
2058
|
const concatTask = task2("Combining audio");
|
|
1843
|
-
await
|
|
2059
|
+
await mkdir3(outputDir, { recursive: true });
|
|
1844
2060
|
const voiceoverPath = join6(outputDir, "voiceover.wav");
|
|
1845
2061
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1846
2062
|
concatTask.succeed("Done");
|
|
@@ -1902,6 +2118,7 @@ var init_types3 = __esm({
|
|
|
1902
2118
|
// src/lib/index.ts
|
|
1903
2119
|
var lib_exports = {};
|
|
1904
2120
|
__export(lib_exports, {
|
|
2121
|
+
CLI_BACKOFF_MS: () => CLI_BACKOFF_MS,
|
|
1905
2122
|
CONFIG_FILE: () => CONFIG_FILE,
|
|
1906
2123
|
CURSOR_CSS: () => CURSOR_CSS,
|
|
1907
2124
|
CURSOR_PNG: () => CURSOR_PNG,
|
|
@@ -1912,9 +2129,11 @@ __export(lib_exports, {
|
|
|
1912
2129
|
FEATURES: () => FEATURES,
|
|
1913
2130
|
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1914
2131
|
LANGUAGE_VOICES: () => WIDE_VOICES,
|
|
2132
|
+
SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
|
|
1915
2133
|
VERSION: () => VERSION,
|
|
1916
2134
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1917
2135
|
applyTransitions: () => applyTransitions,
|
|
2136
|
+
buildConcatArgs: () => buildConcatArgs,
|
|
1918
2137
|
buildKeepSegments: () => buildKeepSegments,
|
|
1919
2138
|
buildStepTransitions: () => buildStepTransitions,
|
|
1920
2139
|
buildSystemPrompt: () => buildSystemPrompt,
|
|
@@ -1928,9 +2147,11 @@ __export(lib_exports, {
|
|
|
1928
2147
|
concatVideoWithOutro: () => concatVideoWithOutro,
|
|
1929
2148
|
crawlPage: () => crawlPage,
|
|
1930
2149
|
estimateWordTimings: () => estimateWordTimings,
|
|
2150
|
+
extractJsonCandidates: () => extractJsonCandidates,
|
|
1931
2151
|
generateASSHeader: () => generateASSHeader,
|
|
1932
2152
|
generateSilence: () => generateSilence,
|
|
1933
2153
|
generateVoiceover: () => generateVoiceover,
|
|
2154
|
+
getAudioCodec: () => getAudioCodec,
|
|
1934
2155
|
getErrorMessage: () => getErrorMessage,
|
|
1935
2156
|
getLanguageName: () => getLanguageName,
|
|
1936
2157
|
getMediaDuration: () => getMediaDuration,
|
|
@@ -1941,20 +2162,25 @@ __export(lib_exports, {
|
|
|
1941
2162
|
hasAccess: () => hasAccess,
|
|
1942
2163
|
hasTrialRemaining: () => hasTrialRemaining,
|
|
1943
2164
|
hexToASS: () => hexToAss,
|
|
2165
|
+
idSelector: () => idSelector,
|
|
1944
2166
|
isCanceling: () => isCanceling,
|
|
1945
2167
|
isExcludedLink: () => isExcludedLink,
|
|
2168
|
+
isHonestWav: () => isHonestWav,
|
|
1946
2169
|
isPaid: () => isPaid,
|
|
1947
2170
|
isVerified: () => isVerified,
|
|
1948
2171
|
loadExistingVoiceover: () => loadExistingVoiceover,
|
|
1949
2172
|
loadVoiceoverMeta: () => loadVoiceoverMeta,
|
|
1950
2173
|
mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
|
|
1951
2174
|
normalizeUrl: () => normalizeUrl,
|
|
2175
|
+
parseAIJson: () => parseAIJson,
|
|
1952
2176
|
parseAIPlanResponse: () => parseAIPlanResponse,
|
|
1953
2177
|
parsePartial: () => parsePartial,
|
|
1954
2178
|
pricingUrl: () => pricingUrl,
|
|
1955
2179
|
registerFreeUser: () => registerFreeUser,
|
|
2180
|
+
repairUnicodeEscapes: () => repairUnicodeEscapes,
|
|
1956
2181
|
resolveVoice: () => resolveVoice,
|
|
1957
2182
|
rgbaToHex: () => rgbaToHex,
|
|
2183
|
+
salvageTruncatedJson: () => salvageTruncatedJson,
|
|
1958
2184
|
shouldSkipLink: () => shouldSkipLink,
|
|
1959
2185
|
shouldWatermark: () => shouldWatermark,
|
|
1960
2186
|
showPaywall: () => showPaywall,
|
|
@@ -1973,6 +2199,7 @@ var init_lib = __esm({
|
|
|
1973
2199
|
init_crawl();
|
|
1974
2200
|
init_cursor();
|
|
1975
2201
|
init_ffmpeg2();
|
|
2202
|
+
init_json_repair();
|
|
1976
2203
|
init_prompts();
|
|
1977
2204
|
init_SparkStripe_feature_gating();
|
|
1978
2205
|
init_scraping();
|
|
@@ -1996,13 +2223,13 @@ __export(config_loader_exports, {
|
|
|
1996
2223
|
savePlan: () => savePlan,
|
|
1997
2224
|
saveUserData: () => saveUserData
|
|
1998
2225
|
});
|
|
1999
|
-
import { existsSync as
|
|
2000
|
-
import { mkdir as
|
|
2226
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2227
|
+
import { mkdir as mkdir4, readFile as readFile2, rm, writeFile as writeFile4 } from "fs/promises";
|
|
2001
2228
|
import { homedir as homedir2 } from "os";
|
|
2002
2229
|
import { dirname, join as join7 } from "path";
|
|
2003
2230
|
async function loadConfig(_url, langOverride) {
|
|
2004
2231
|
const configPath = join7(process.cwd(), CONFIG_FILE);
|
|
2005
|
-
if (
|
|
2232
|
+
if (existsSync6(configPath)) {
|
|
2006
2233
|
try {
|
|
2007
2234
|
const content = await readFile2(configPath, "utf-8");
|
|
2008
2235
|
const userConfig = JSON.parse(content);
|
|
@@ -2052,15 +2279,15 @@ async function loadConfig(_url, langOverride) {
|
|
|
2052
2279
|
}
|
|
2053
2280
|
async function prepareWorkDir() {
|
|
2054
2281
|
const workDir = join7(process.cwd(), WORK_DIR);
|
|
2055
|
-
if (
|
|
2282
|
+
if (existsSync6(workDir)) {
|
|
2056
2283
|
await rm(workDir, { recursive: true });
|
|
2057
2284
|
}
|
|
2058
|
-
await
|
|
2285
|
+
await mkdir4(workDir, { recursive: true });
|
|
2059
2286
|
return workDir;
|
|
2060
2287
|
}
|
|
2061
2288
|
async function loadUserData() {
|
|
2062
2289
|
try {
|
|
2063
|
-
if (
|
|
2290
|
+
if (existsSync6(USER_DATA_FILE)) {
|
|
2064
2291
|
return JSON.parse(await readFile2(USER_DATA_FILE, "utf-8"));
|
|
2065
2292
|
}
|
|
2066
2293
|
} catch {
|
|
@@ -2068,8 +2295,8 @@ async function loadUserData() {
|
|
|
2068
2295
|
return {};
|
|
2069
2296
|
}
|
|
2070
2297
|
async function saveUserData(userData) {
|
|
2071
|
-
if (!
|
|
2072
|
-
await
|
|
2298
|
+
if (!existsSync6(USER_DATA_DIR)) {
|
|
2299
|
+
await mkdir4(USER_DATA_DIR, { recursive: true });
|
|
2073
2300
|
}
|
|
2074
2301
|
const existing = await loadUserData();
|
|
2075
2302
|
const merged = { ...existing, ...userData };
|
|
@@ -2122,7 +2349,7 @@ function parseCaptionCSS(css) {
|
|
|
2122
2349
|
}
|
|
2123
2350
|
async function loadCaptions() {
|
|
2124
2351
|
const captionsPath = join7(process.cwd(), "captions.css");
|
|
2125
|
-
if (!
|
|
2352
|
+
if (!existsSync6(captionsPath)) {
|
|
2126
2353
|
await writeFile4(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2127
2354
|
return DEFAULT_CAPTIONS;
|
|
2128
2355
|
}
|
|
@@ -2138,7 +2365,7 @@ async function savePlan(plan, planPath) {
|
|
|
2138
2365
|
...plan,
|
|
2139
2366
|
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2140
2367
|
};
|
|
2141
|
-
await
|
|
2368
|
+
await mkdir4(dirname(planPath), { recursive: true });
|
|
2142
2369
|
await writeFile4(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2143
2370
|
}
|
|
2144
2371
|
var WORK_DIR, USER_DATA_DIR, USER_DATA_FILE, DEFAULT_CAPTIONS_CSS;
|
|
@@ -2246,16 +2473,16 @@ function fileResult(output, data, poster) {
|
|
|
2246
2473
|
}
|
|
2247
2474
|
|
|
2248
2475
|
// src/mcp/tools.ts
|
|
2249
|
-
import { readdirSync as
|
|
2250
|
-
import { join as
|
|
2476
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
2477
|
+
import { join as join23 } from "path";
|
|
2251
2478
|
import { WIDE_VOICES as WIDE_VOICES2 } from "@spark-apps/video-kit";
|
|
2252
2479
|
|
|
2253
2480
|
// src/browser/auth-state.ts
|
|
2254
|
-
import { existsSync as existsSync2 } from "fs";
|
|
2481
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2255
2482
|
import { chmod, mkdir, readdir, unlink, writeFile } from "fs/promises";
|
|
2256
2483
|
import { homedir } from "os";
|
|
2257
2484
|
import { join } from "path";
|
|
2258
|
-
var AUTH_DIR = join(homedir(), ".quickpeek", "auth");
|
|
2485
|
+
var AUTH_DIR = process.env.QUICKPEEK_AUTH_DIR || join(homedir(), ".quickpeek", "auth");
|
|
2259
2486
|
function hostOf(url) {
|
|
2260
2487
|
try {
|
|
2261
2488
|
return new URL(url).host;
|
|
@@ -2267,145 +2494,551 @@ function authStatePath(url) {
|
|
|
2267
2494
|
const host = hostOf(url);
|
|
2268
2495
|
return host ? join(AUTH_DIR, `${host}.json`) : null;
|
|
2269
2496
|
}
|
|
2497
|
+
function authMetaPath(url) {
|
|
2498
|
+
const host = hostOf(url);
|
|
2499
|
+
return host ? join(AUTH_DIR, `${host}.meta.json`) : null;
|
|
2500
|
+
}
|
|
2270
2501
|
function findAuthState(url) {
|
|
2271
2502
|
const path = authStatePath(url);
|
|
2272
2503
|
return path && existsSync2(path) ? path : void 0;
|
|
2273
2504
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
suffix: " \u{1F440}"
|
|
2294
|
-
};
|
|
2295
|
-
function renderLogo(config = {}) {
|
|
2296
|
-
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2297
|
-
const ascii = figlet.textSync(resolved.text, {
|
|
2298
|
-
font: resolved.font,
|
|
2299
|
-
horizontalLayout: resolved.horizontalLayout
|
|
2300
|
-
});
|
|
2301
|
-
const colored = gradient(resolved.gradientColors)(ascii);
|
|
2302
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
// src/help.ts
|
|
2306
|
-
init_config_loader();
|
|
2307
|
-
var cmd = chalk.cyan;
|
|
2308
|
-
var arg = chalk.green;
|
|
2309
|
-
var opt = chalk.magenta;
|
|
2310
|
-
var desc = chalk.white;
|
|
2311
|
-
var ex = chalk.dim;
|
|
2312
|
-
function showLogo() {
|
|
2313
|
-
const logo = renderLogo();
|
|
2314
|
-
console.log(logo);
|
|
2315
|
-
console.log(chalk.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
2316
|
-
console.log(chalk.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
2317
|
-
}
|
|
2318
|
-
function formatUsageLines(status, tier) {
|
|
2319
|
-
const lines = [];
|
|
2320
|
-
lines.push(`Videos generated: ${status.usage_count}`);
|
|
2321
|
-
if (tier !== "pro" && status.trial) {
|
|
2322
|
-
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
2323
|
-
const used = max_uses - uses_remaining;
|
|
2324
|
-
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
2325
|
-
if (!is_expired && days_remaining > 0) {
|
|
2326
|
-
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
if (tier === "pro" && status.subscription) {
|
|
2330
|
-
const end = status.subscription.current_period_end;
|
|
2331
|
-
if (status.subscription.is_lifetime) {
|
|
2332
|
-
lines.push("Lifetime access");
|
|
2333
|
-
} else if (isCanceling(status)) {
|
|
2334
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2335
|
-
lines.push(`Cancels: ${date}`);
|
|
2336
|
-
} else if (end) {
|
|
2337
|
-
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2338
|
-
lines.push(`Renews: ${date}`);
|
|
2339
|
-
}
|
|
2505
|
+
async function saveAuthState(url, state, startUrl) {
|
|
2506
|
+
const path = authStatePath(url);
|
|
2507
|
+
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2508
|
+
await mkdir(AUTH_DIR, { recursive: true });
|
|
2509
|
+
await writeFile(path, JSON.stringify(state, null, 2));
|
|
2510
|
+
await chmod(path, 384);
|
|
2511
|
+
if (startUrl) {
|
|
2512
|
+
await writeFile(authMetaPath(url), JSON.stringify({ startUrl, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2513
|
+
}
|
|
2514
|
+
return path;
|
|
2515
|
+
}
|
|
2516
|
+
function findStartUrl(url) {
|
|
2517
|
+
const path = authMetaPath(url);
|
|
2518
|
+
if (!path || !existsSync2(path)) return void 0;
|
|
2519
|
+
try {
|
|
2520
|
+
const startUrl = JSON.parse(readFileSync2(path, "utf8")).startUrl;
|
|
2521
|
+
return typeof startUrl === "string" && startUrl.startsWith("http") ? startUrl : void 0;
|
|
2522
|
+
} catch {
|
|
2523
|
+
return void 0;
|
|
2340
2524
|
}
|
|
2341
|
-
return lines;
|
|
2342
2525
|
}
|
|
2343
|
-
|
|
2344
|
-
// src/commands/record-cmd.ts
|
|
2345
|
-
init_lib();
|
|
2346
|
-
init_config_loader();
|
|
2347
|
-
|
|
2348
|
-
// src/utils/logging.ts
|
|
2349
|
-
import { appendFile, writeFile as writeFile5 } from "fs/promises";
|
|
2350
|
-
import { join as join8 } from "path";
|
|
2351
|
-
var LOG_FILE_NAME = "quickpeek.log";
|
|
2352
|
-
var logFileInitialized = false;
|
|
2353
|
-
var logFilePath = LOG_FILE_NAME;
|
|
2354
|
-
async function writeLogEntry(entry) {
|
|
2526
|
+
function resolveStartUrl(url) {
|
|
2355
2527
|
try {
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
await appendFile(logFilePath, entry);
|
|
2361
|
-
}
|
|
2528
|
+
const parsed = new URL(url);
|
|
2529
|
+
const bareOrigin = (parsed.pathname === "/" || parsed.pathname === "") && !parsed.search && !parsed.hash;
|
|
2530
|
+
if (!bareOrigin || !findAuthState(url)) return url;
|
|
2531
|
+
return findStartUrl(url) ?? url;
|
|
2362
2532
|
} catch {
|
|
2533
|
+
return url;
|
|
2363
2534
|
}
|
|
2364
2535
|
}
|
|
2365
|
-
async function setLogFilePath(outputDir) {
|
|
2366
|
-
logFilePath = join8(outputDir, LOG_FILE_NAME);
|
|
2367
|
-
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2368
|
-
${"=".repeat(60)}
|
|
2369
2536
|
|
|
2370
|
-
|
|
2537
|
+
// src/browser/launch.ts
|
|
2538
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
2539
|
+
import { chromium as chromium2 } from "playwright";
|
|
2540
|
+
|
|
2541
|
+
// src/browser/wsl.ts
|
|
2542
|
+
import { execSync, spawn } from "child_process";
|
|
2543
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2544
|
+
import { request } from "http";
|
|
2545
|
+
import { chromium } from "playwright";
|
|
2546
|
+
function isWSL() {
|
|
2547
|
+
if (process.platform !== "linux") return false;
|
|
2371
2548
|
try {
|
|
2372
|
-
|
|
2373
|
-
|
|
2549
|
+
const r = execSync("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2550
|
+
return r.includes("microsoft") || r.includes("wsl");
|
|
2374
2551
|
} catch {
|
|
2552
|
+
return false;
|
|
2375
2553
|
}
|
|
2376
2554
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
`);
|
|
2555
|
+
function tryEnableWSLInterop() {
|
|
2556
|
+
return existsSync3("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2380
2557
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2558
|
+
function wslToWindowsPath(wslPath) {
|
|
2559
|
+
if (!wslPath) return wslPath;
|
|
2560
|
+
const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
|
|
2561
|
+
if (!m) return wslPath;
|
|
2562
|
+
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2384
2563
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
}
|
|
2391
|
-
errorMsg = JSON.stringify(error, null, 2);
|
|
2392
|
-
} else {
|
|
2393
|
-
errorMsg = String(error);
|
|
2564
|
+
function findExe(whereExeQuery, ...hardcoded) {
|
|
2565
|
+
for (const p of hardcoded) if (existsSync3(p)) return p;
|
|
2566
|
+
try {
|
|
2567
|
+
const line = execSync(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2568
|
+
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2569
|
+
} catch {
|
|
2394
2570
|
}
|
|
2395
|
-
|
|
2396
|
-
${errorMsg}
|
|
2397
|
-
|
|
2398
|
-
`);
|
|
2571
|
+
return null;
|
|
2399
2572
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2573
|
+
var findWindowsChrome = () => findExe(
|
|
2574
|
+
"chrome.exe",
|
|
2575
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2576
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
|
2577
|
+
);
|
|
2578
|
+
var findWindowsNode = () => findExe("node.exe");
|
|
2579
|
+
function getWindowsHostIP() {
|
|
2580
|
+
try {
|
|
2581
|
+
const m = execSync("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2582
|
+
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2583
|
+
);
|
|
2584
|
+
if (m?.[1]) return m[1];
|
|
2585
|
+
} catch {
|
|
2586
|
+
}
|
|
2587
|
+
try {
|
|
2588
|
+
const m = readFileSync3("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2589
|
+
if (m?.[1]) return m[1];
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
return "127.0.0.1";
|
|
2404
2593
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2594
|
+
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2595
|
+
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
2596
|
+
return new Promise((resolve10, reject) => {
|
|
2597
|
+
const proc = spawn(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2598
|
+
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2599
|
+
let buf = "";
|
|
2600
|
+
proc.stdout.on("data", (d) => {
|
|
2601
|
+
buf += d.toString();
|
|
2602
|
+
const m = buf.match(/PORT:(\d+)/);
|
|
2603
|
+
if (m) {
|
|
2604
|
+
clearTimeout(timer);
|
|
2605
|
+
resolve10({ proc, port: +m[1] });
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
proc.on("error", (e) => {
|
|
2609
|
+
clearTimeout(timer);
|
|
2610
|
+
reject(e);
|
|
2611
|
+
});
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
function waitForChrome(host, port) {
|
|
2615
|
+
return new Promise((resolve10, reject) => {
|
|
2616
|
+
const deadline = Date.now() + 2e4;
|
|
2617
|
+
const poll = () => {
|
|
2618
|
+
if (Date.now() > deadline) {
|
|
2619
|
+
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const req = request({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2623
|
+
res.resume();
|
|
2624
|
+
resolve10();
|
|
2625
|
+
});
|
|
2626
|
+
req.on("error", () => setTimeout(poll, 600));
|
|
2627
|
+
req.setTimeout(1e3, () => {
|
|
2628
|
+
req.destroy();
|
|
2629
|
+
setTimeout(poll, 600);
|
|
2630
|
+
});
|
|
2631
|
+
req.end();
|
|
2632
|
+
};
|
|
2633
|
+
poll();
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
2637
|
+
const debugPort = options.debugPort ?? 9222;
|
|
2638
|
+
const chromePath = findWindowsChrome();
|
|
2639
|
+
if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
|
|
2640
|
+
const nodePath = findWindowsNode();
|
|
2641
|
+
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2642
|
+
const winUserData = wslToWindowsPath(userDataDir);
|
|
2643
|
+
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2644
|
+
const chromeProc = spawn(
|
|
2645
|
+
chromePath,
|
|
2646
|
+
[
|
|
2647
|
+
`--remote-debugging-port=${debugPort}`,
|
|
2648
|
+
`--user-data-dir=${winUserData}`,
|
|
2649
|
+
...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
|
|
2650
|
+
"--no-first-run",
|
|
2651
|
+
"--no-default-browser-check",
|
|
2652
|
+
"--disable-background-networking",
|
|
2653
|
+
"--disable-sync",
|
|
2654
|
+
"--disable-features=TranslateUI",
|
|
2655
|
+
options.startMaximized ? "--start-maximized" : "--start-minimized"
|
|
2656
|
+
],
|
|
2657
|
+
{ stdio: "ignore", detached: true }
|
|
2658
|
+
);
|
|
2659
|
+
chromeProc.unref();
|
|
2660
|
+
const windowsHost = getWindowsHostIP();
|
|
2661
|
+
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2662
|
+
try {
|
|
2663
|
+
await waitForChrome(windowsHost, proxyPort);
|
|
2664
|
+
const browser = await chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2665
|
+
return {
|
|
2666
|
+
browser,
|
|
2667
|
+
cleanup: async () => {
|
|
2668
|
+
try {
|
|
2669
|
+
await browser.close();
|
|
2670
|
+
} catch {
|
|
2671
|
+
}
|
|
2672
|
+
relay.kill();
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
} catch (err) {
|
|
2676
|
+
relay.kill();
|
|
2677
|
+
throw err;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// src/browser/launch.ts
|
|
2682
|
+
function recordingViewport(config) {
|
|
2683
|
+
const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
|
|
2684
|
+
return {
|
|
2685
|
+
width: Math.round(config.video.width / zoom),
|
|
2686
|
+
height: Math.round(config.video.height / zoom)
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
2690
|
+
const extensionPath = config.browser?.extension;
|
|
2691
|
+
const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
|
|
2692
|
+
const viewport = recordingViewport(config);
|
|
2693
|
+
const scaleOpts = zoom === 1 ? {} : { deviceScaleFactor: zoom };
|
|
2694
|
+
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
2695
|
+
const cspOpts = { bypassCSP: true };
|
|
2696
|
+
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
2697
|
+
let context;
|
|
2698
|
+
let browser = null;
|
|
2699
|
+
let chromeSession = null;
|
|
2700
|
+
if (extensionPath) {
|
|
2701
|
+
await mkdir2(userDataDir, { recursive: true });
|
|
2702
|
+
const persistentArgs = [
|
|
2703
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
2704
|
+
`--load-extension=${extensionPath}`,
|
|
2705
|
+
"--window-position=-32000,-32000",
|
|
2706
|
+
"--window-size=1,1"
|
|
2707
|
+
];
|
|
2708
|
+
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
2709
|
+
if (useWindowsChrome) {
|
|
2710
|
+
try {
|
|
2711
|
+
chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...scaleOpts, ...videoOpts });
|
|
2712
|
+
const contexts = chromeSession.browser.contexts();
|
|
2713
|
+
context = contexts[0] || await chromeSession.browser.newContext({
|
|
2714
|
+
viewport,
|
|
2715
|
+
...scaleOpts,
|
|
2716
|
+
...videoOpts,
|
|
2717
|
+
...cspOpts,
|
|
2718
|
+
acceptDownloads: true
|
|
2719
|
+
});
|
|
2720
|
+
} catch {
|
|
2721
|
+
context = await chromium2.launchPersistentContext(userDataDir, {
|
|
2722
|
+
headless: false,
|
|
2723
|
+
args: persistentArgs,
|
|
2724
|
+
viewport,
|
|
2725
|
+
...scaleOpts,
|
|
2726
|
+
...videoOpts,
|
|
2727
|
+
...cspOpts,
|
|
2728
|
+
acceptDownloads: true
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
} else {
|
|
2732
|
+
context = await chromium2.launchPersistentContext(userDataDir, {
|
|
2733
|
+
headless: false,
|
|
2734
|
+
args: persistentArgs,
|
|
2735
|
+
viewport,
|
|
2736
|
+
...scaleOpts,
|
|
2737
|
+
...videoOpts,
|
|
2738
|
+
...cspOpts,
|
|
2739
|
+
acceptDownloads: true
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
} else {
|
|
2743
|
+
if (process.env.QP_HEADED === "1") {
|
|
2744
|
+
const headedOpts = {
|
|
2745
|
+
headless: false,
|
|
2746
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
2747
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
2748
|
+
};
|
|
2749
|
+
try {
|
|
2750
|
+
browser = await chromium2.launch({ ...headedOpts, channel: "chrome" });
|
|
2751
|
+
} catch {
|
|
2752
|
+
browser = await chromium2.launch(headedOpts);
|
|
2753
|
+
}
|
|
2754
|
+
} else {
|
|
2755
|
+
browser = await chromium2.launch({ headless: true });
|
|
2756
|
+
}
|
|
2757
|
+
context = await browser.newContext({
|
|
2758
|
+
viewport,
|
|
2759
|
+
...scaleOpts,
|
|
2760
|
+
...videoOpts,
|
|
2761
|
+
...authOpts,
|
|
2762
|
+
...cspOpts,
|
|
2763
|
+
acceptDownloads: true
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
await hideDevOverlays(context);
|
|
2767
|
+
return { context, browser, chromeSession };
|
|
2768
|
+
}
|
|
2769
|
+
async function hideDevOverlays(context) {
|
|
2770
|
+
await context.addInitScript(() => {
|
|
2771
|
+
const css = "nextjs-portal,vite-error-overlay,vite-plugin-checker-error-overlay{display:none!important;pointer-events:none!important}";
|
|
2772
|
+
const add = () => {
|
|
2773
|
+
const style = document.createElement("style");
|
|
2774
|
+
style.textContent = css;
|
|
2775
|
+
document.head?.appendChild(style);
|
|
2776
|
+
};
|
|
2777
|
+
if (document.head) add();
|
|
2778
|
+
else document.addEventListener("DOMContentLoaded", add, { once: true });
|
|
2779
|
+
}).catch(() => {
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
async function launchSignInBrowser() {
|
|
2783
|
+
const opts = {
|
|
2784
|
+
headless: false,
|
|
2785
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
2786
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
2787
|
+
};
|
|
2788
|
+
try {
|
|
2789
|
+
return await chromium2.launch({ ...opts, channel: "chrome" });
|
|
2790
|
+
} catch {
|
|
2791
|
+
return await chromium2.launch(opts);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
async function cleanupBrowserSession(session) {
|
|
2795
|
+
if (session.chromeSession) {
|
|
2796
|
+
await session.chromeSession.cleanup();
|
|
2797
|
+
} else {
|
|
2798
|
+
await session.context.close();
|
|
2799
|
+
if (session.browser) await session.browser.close();
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/browser/auth-session.ts
|
|
2804
|
+
var pending = null;
|
|
2805
|
+
function pendingSignIn() {
|
|
2806
|
+
return pending?.url ?? null;
|
|
2807
|
+
}
|
|
2808
|
+
async function openSignIn(url) {
|
|
2809
|
+
await cancelSignIn();
|
|
2810
|
+
const browser = await launchSignInBrowser();
|
|
2811
|
+
const context = await browser.newContext();
|
|
2812
|
+
const page = await context.newPage();
|
|
2813
|
+
pending = { browser, context, url };
|
|
2814
|
+
try {
|
|
2815
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
2816
|
+
} catch {
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
function sameSite(a, b) {
|
|
2820
|
+
if (a === b) return true;
|
|
2821
|
+
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
2822
|
+
return a.includes(".") && b.includes(".") && tail(a) === tail(b);
|
|
2823
|
+
}
|
|
2824
|
+
function captureStartUrl(context, targetUrl) {
|
|
2825
|
+
const targetHost = new URL(targetUrl).host;
|
|
2826
|
+
const pages = context.pages();
|
|
2827
|
+
for (let i = pages.length - 1; i >= 0; i--) {
|
|
2828
|
+
const pageUrl = pages[i].url();
|
|
2829
|
+
if (!pageUrl.startsWith("http")) continue;
|
|
2830
|
+
if (sameSite(new URL(pageUrl).host, targetHost)) return pageUrl;
|
|
2831
|
+
}
|
|
2832
|
+
return void 0;
|
|
2833
|
+
}
|
|
2834
|
+
async function saveSignIn() {
|
|
2835
|
+
if (!pending) throw new Error("No sign-in window is open. Call start_auth first.");
|
|
2836
|
+
const { context, url } = pending;
|
|
2837
|
+
try {
|
|
2838
|
+
const startUrl = captureStartUrl(context, url);
|
|
2839
|
+
const path = await saveAuthState(url, await context.storageState(), startUrl);
|
|
2840
|
+
return { host: new URL(url).host, path, ...startUrl ? { startUrl } : {} };
|
|
2841
|
+
} finally {
|
|
2842
|
+
await cancelSignIn();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
async function cancelSignIn() {
|
|
2846
|
+
const open = pending;
|
|
2847
|
+
pending = null;
|
|
2848
|
+
await open?.browser.close().catch(() => {
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/commands/edit-cmd.ts
|
|
2853
|
+
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
2854
|
+
import { join as join10 } from "path";
|
|
2855
|
+
import chalk7 from "chalk";
|
|
2856
|
+
|
|
2857
|
+
// src/help.ts
|
|
2858
|
+
init_lib();
|
|
2859
|
+
import chalk from "chalk";
|
|
2860
|
+
|
|
2861
|
+
// src/logo.ts
|
|
2862
|
+
import figlet from "figlet";
|
|
2863
|
+
import gradient from "gradient-string";
|
|
2864
|
+
var DEFAULT_LOGO_CONFIG = {
|
|
2865
|
+
text: "QuickPeek",
|
|
2866
|
+
font: "Slant",
|
|
2867
|
+
horizontalLayout: "default",
|
|
2868
|
+
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2869
|
+
suffix: " \u{1F440}"
|
|
2870
|
+
};
|
|
2871
|
+
function renderLogo(config = {}) {
|
|
2872
|
+
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2873
|
+
const ascii = figlet.textSync(resolved.text, {
|
|
2874
|
+
font: resolved.font,
|
|
2875
|
+
horizontalLayout: resolved.horizontalLayout
|
|
2876
|
+
});
|
|
2877
|
+
const colored = gradient(resolved.gradientColors)(ascii);
|
|
2878
|
+
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
// src/help.ts
|
|
2882
|
+
init_config_loader();
|
|
2883
|
+
var cmd = chalk.cyan;
|
|
2884
|
+
var arg = chalk.green;
|
|
2885
|
+
var opt = chalk.magenta;
|
|
2886
|
+
var desc = chalk.white;
|
|
2887
|
+
var ex = chalk.dim;
|
|
2888
|
+
function showLogo() {
|
|
2889
|
+
const logo = renderLogo();
|
|
2890
|
+
console.log(logo);
|
|
2891
|
+
console.log(chalk.dim(` v${VERSION} - Auto-generate demo videos with AI`));
|
|
2892
|
+
console.log(chalk.italic.dim(" Because Tutorial Videos are Overrated!\n"));
|
|
2893
|
+
}
|
|
2894
|
+
function formatUsageLines(status, tier) {
|
|
2895
|
+
const lines = [];
|
|
2896
|
+
lines.push(`Videos generated: ${status.usage_count}`);
|
|
2897
|
+
if (tier !== "pro" && status.trial) {
|
|
2898
|
+
const { uses_remaining, max_uses, days_remaining, is_expired } = status.trial;
|
|
2899
|
+
const used = max_uses - uses_remaining;
|
|
2900
|
+
lines.push(`Trial: ${used}/${max_uses} uses${is_expired ? " (expired)" : ` (${uses_remaining} remaining)`}`);
|
|
2901
|
+
if (!is_expired && days_remaining > 0) {
|
|
2902
|
+
lines.push(`Trial ends in ${days_remaining} day${days_remaining === 1 ? "" : "s"}`);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (tier === "pro" && status.subscription) {
|
|
2906
|
+
const end = status.subscription.current_period_end;
|
|
2907
|
+
if (status.subscription.is_lifetime) {
|
|
2908
|
+
lines.push("Lifetime access");
|
|
2909
|
+
} else if (isCanceling(status)) {
|
|
2910
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2911
|
+
lines.push(`Cancels: ${date}`);
|
|
2912
|
+
} else if (end) {
|
|
2913
|
+
const date = new Date(end).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
2914
|
+
lines.push(`Renews: ${date}`);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
return lines;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// src/lib/browser-open.ts
|
|
2921
|
+
import { exec as exec2 } from "child_process";
|
|
2922
|
+
function editorBaseUrl() {
|
|
2923
|
+
return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
|
|
2924
|
+
}
|
|
2925
|
+
function openInBrowser(url) {
|
|
2926
|
+
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2927
|
+
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2928
|
+
return new Promise((resolve10) => {
|
|
2929
|
+
exec2(command, () => resolve10());
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/lib/plan-validation.ts
|
|
2934
|
+
var INVALID_TARGETS = [
|
|
2935
|
+
/\.png$/i,
|
|
2936
|
+
/\.jpg$/i,
|
|
2937
|
+
/\.gif$/i,
|
|
2938
|
+
/\.webp$/i,
|
|
2939
|
+
/\.svg$/i,
|
|
2940
|
+
/sprite_sheet/i,
|
|
2941
|
+
/downloaded/i
|
|
2942
|
+
];
|
|
2943
|
+
var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
|
|
2944
|
+
function isFilePath(value) {
|
|
2945
|
+
if (!value) return false;
|
|
2946
|
+
return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
|
|
2947
|
+
}
|
|
2948
|
+
function isInvalidTarget(target) {
|
|
2949
|
+
return INVALID_TARGETS.some((pattern) => pattern.test(target));
|
|
2950
|
+
}
|
|
2951
|
+
function validateStep(step) {
|
|
2952
|
+
if (!step?.action) {
|
|
2953
|
+
return "Missing action";
|
|
2954
|
+
}
|
|
2955
|
+
if (step.action === "navigate") {
|
|
2956
|
+
if (!step.target) return "Navigate requires a URL target";
|
|
2957
|
+
if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
|
|
2958
|
+
return "Navigate target must be a valid URL";
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
if (step.action === "media") {
|
|
2962
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
2963
|
+
return "Media step requires a file (image or video)";
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
if (step.action === "upload") {
|
|
2967
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
2968
|
+
return "Upload step requires a file";
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
|
|
2972
|
+
if (!step.target) return `${step.action} requires a target`;
|
|
2973
|
+
if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
|
|
2974
|
+
}
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// src/lib/plan-file.ts
|
|
2979
|
+
var MAX_HANDOFF_URL_CHARS = 32e3;
|
|
2980
|
+
function serializePlan(plan) {
|
|
2981
|
+
return `${JSON.stringify(plan, null, 2)}
|
|
2982
|
+
`;
|
|
2983
|
+
}
|
|
2984
|
+
function parsePlanText(text) {
|
|
2985
|
+
let raw;
|
|
2986
|
+
try {
|
|
2987
|
+
raw = JSON.parse(text);
|
|
2988
|
+
} catch (e) {
|
|
2989
|
+
return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
2990
|
+
}
|
|
2991
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2992
|
+
return { ok: false, error: "A plan must be a JSON object." };
|
|
2993
|
+
}
|
|
2994
|
+
const candidate = raw;
|
|
2995
|
+
if (typeof candidate.url !== "string" || !candidate.url.trim()) {
|
|
2996
|
+
return { ok: false, error: "A plan needs a `url` to record." };
|
|
2997
|
+
}
|
|
2998
|
+
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
|
|
2999
|
+
return { ok: false, error: "A plan needs at least one step." };
|
|
3000
|
+
}
|
|
3001
|
+
const steps = [];
|
|
3002
|
+
for (const [index, step] of candidate.steps.entries()) {
|
|
3003
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
3004
|
+
return { ok: false, error: `Step ${index + 1} is not an object.` };
|
|
3005
|
+
}
|
|
3006
|
+
const typed = step;
|
|
3007
|
+
if (typeof typed.action !== "string" || !typed.action.trim()) {
|
|
3008
|
+
return { ok: false, error: `Step ${index + 1} has no action.` };
|
|
3009
|
+
}
|
|
3010
|
+
const failure = validateStep(typed);
|
|
3011
|
+
if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
|
|
3012
|
+
steps.push({ ...typed, id: typed.id ?? index + 1 });
|
|
3013
|
+
}
|
|
3014
|
+
return {
|
|
3015
|
+
ok: true,
|
|
3016
|
+
plan: {
|
|
3017
|
+
url: candidate.url,
|
|
3018
|
+
title: candidate.title ?? "",
|
|
3019
|
+
description: candidate.description ?? "",
|
|
3020
|
+
...candidate.lang ? { lang: candidate.lang } : {},
|
|
3021
|
+
steps
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
function toBase64Url(text) {
|
|
3026
|
+
const bytes = new TextEncoder().encode(text);
|
|
3027
|
+
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
3028
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
|
|
3029
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3030
|
+
}
|
|
3031
|
+
function encodePlan(plan) {
|
|
3032
|
+
return toBase64Url(serializePlan(plan));
|
|
3033
|
+
}
|
|
3034
|
+
function planEditorUrl(baseUrl, plan) {
|
|
3035
|
+
const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
|
|
3036
|
+
return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// src/utils/ui.ts
|
|
3040
|
+
import { resolve as resolve4 } from "path";
|
|
3041
|
+
import { createInterface } from "readline";
|
|
2409
3042
|
import chalk2 from "chalk";
|
|
2410
3043
|
import ora from "ora";
|
|
2411
3044
|
var INDENT = " ";
|
|
@@ -2413,6 +3046,9 @@ var autoYes = false;
|
|
|
2413
3046
|
function setAutoYes(value) {
|
|
2414
3047
|
autoYes = value;
|
|
2415
3048
|
}
|
|
3049
|
+
function isAutoYes() {
|
|
3050
|
+
return autoYes;
|
|
3051
|
+
}
|
|
2416
3052
|
function task(text) {
|
|
2417
3053
|
return ora({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2418
3054
|
}
|
|
@@ -2446,7 +3082,7 @@ function promptInput(prompt2) {
|
|
|
2446
3082
|
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2447
3083
|
function formatClickablePath(filePath) {
|
|
2448
3084
|
const absolutePath = resolve4(filePath);
|
|
2449
|
-
if (_isWSL) {
|
|
3085
|
+
if (_isWSL || process.platform !== "win32") {
|
|
2450
3086
|
return absolutePath;
|
|
2451
3087
|
}
|
|
2452
3088
|
let formatted = absolutePath;
|
|
@@ -2462,12 +3098,12 @@ function formatDuration(ms) {
|
|
|
2462
3098
|
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
|
2463
3099
|
}
|
|
2464
3100
|
|
|
2465
|
-
// src/commands/
|
|
3101
|
+
// src/commands/utils/record-helpers.ts
|
|
3102
|
+
import chalk6 from "chalk";
|
|
2466
3103
|
init_lib();
|
|
2467
|
-
import chalk8 from "chalk";
|
|
2468
3104
|
|
|
2469
3105
|
// src/plan.ts
|
|
2470
|
-
import { mkdir as
|
|
3106
|
+
import { mkdir as mkdir6, readFile as readFile3, writeFile as writeFile6 } from "fs/promises";
|
|
2471
3107
|
import { join as join9 } from "path";
|
|
2472
3108
|
import chalk5 from "chalk";
|
|
2473
3109
|
import { chromium as chromium4 } from "playwright";
|
|
@@ -2478,19 +3114,11 @@ import { createInterface as createInterface2 } from "readline";
|
|
|
2478
3114
|
import chalk4 from "chalk";
|
|
2479
3115
|
|
|
2480
3116
|
// src/utils/browser-auth.ts
|
|
2481
|
-
init_SparkStripe_feature_gating();
|
|
2482
3117
|
import { createServer } from "http";
|
|
2483
3118
|
import { URL as URL2 } from "url";
|
|
2484
3119
|
import chalk3 from "chalk";
|
|
3120
|
+
init_SparkStripe_feature_gating();
|
|
2485
3121
|
var TIMEOUT_MS = 12e4;
|
|
2486
|
-
async function openBrowser(url) {
|
|
2487
|
-
const { exec: exec2 } = await import("child_process");
|
|
2488
|
-
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2489
|
-
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2490
|
-
return new Promise((resolve10) => {
|
|
2491
|
-
exec2(command, () => resolve10());
|
|
2492
|
-
});
|
|
2493
|
-
}
|
|
2494
3122
|
async function browserLogin() {
|
|
2495
3123
|
return new Promise((resolve10) => {
|
|
2496
3124
|
let server;
|
|
@@ -2544,7 +3172,7 @@ async function browserLogin() {
|
|
|
2544
3172
|
console.log(chalk3.dim(` ${url}`));
|
|
2545
3173
|
console.log();
|
|
2546
3174
|
console.log(chalk3.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2547
|
-
|
|
3175
|
+
void openInBrowser(url);
|
|
2548
3176
|
});
|
|
2549
3177
|
timer = setTimeout(() => {
|
|
2550
3178
|
console.log();
|
|
@@ -2554,302 +3182,293 @@ async function browserLogin() {
|
|
|
2554
3182
|
});
|
|
2555
3183
|
}
|
|
2556
3184
|
|
|
2557
|
-
// src/
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
}
|
|
2571
|
-
async function setupUserEmail() {
|
|
2572
|
-
console.log();
|
|
2573
|
-
console.log(chalk4.yellow(" \u2709 Account Setup"));
|
|
2574
|
-
console.log(chalk4.dim(" \u2500".repeat(20)));
|
|
2575
|
-
console.log();
|
|
2576
|
-
console.log(chalk4.white(" Sign in with your Google account to track usage"));
|
|
2577
|
-
console.log(chalk4.white(" and unlock Pro features."));
|
|
2578
|
-
const browserEmail = await browserLogin();
|
|
2579
|
-
if (browserEmail) {
|
|
2580
|
-
console.log();
|
|
2581
|
-
console.log(chalk4.green(` \u2713 Signed in as ${browserEmail}`));
|
|
2582
|
-
console.log();
|
|
2583
|
-
return browserEmail;
|
|
2584
|
-
}
|
|
2585
|
-
console.log();
|
|
2586
|
-
console.log(chalk4.dim(" Or enter your email manually:"));
|
|
2587
|
-
const email = await prompt(chalk4.white(" Email: "));
|
|
2588
|
-
if (!email) {
|
|
2589
|
-
console.log();
|
|
2590
|
-
console.log(chalk4.red(" No email provided. Exiting."));
|
|
2591
|
-
process.exit(1);
|
|
2592
|
-
}
|
|
2593
|
-
if (!email.includes("@") || !email.includes(".")) {
|
|
2594
|
-
console.log();
|
|
2595
|
-
console.log(chalk4.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
2596
|
-
const proceed = await prompt(chalk4.white(" Continue anyway? ") + chalk4.dim("[y/N] "));
|
|
2597
|
-
if (proceed.toLowerCase() !== "y") {
|
|
2598
|
-
process.exit(1);
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
console.log();
|
|
2602
|
-
console.log(chalk4.green(" \u2713 Email registered"));
|
|
2603
|
-
console.log();
|
|
2604
|
-
return email;
|
|
2605
|
-
}
|
|
2606
|
-
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
|
|
2607
|
-
const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier);
|
|
2608
|
-
if (!result.success) {
|
|
2609
|
-
await logError2("AI API", new Error(result.error));
|
|
2610
|
-
if (result.isRateLimit) {
|
|
2611
|
-
console.log();
|
|
2612
|
-
console.log(chalk4.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2613
|
-
console.log();
|
|
2614
|
-
console.log(chalk4.dim(" Spark AI daily limit exceeded. You may have hit your quota."));
|
|
2615
|
-
console.log(chalk4.dim(" Options:"));
|
|
2616
|
-
if (result.retryAfterSeconds) {
|
|
2617
|
-
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
2618
|
-
console.log(chalk4.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
2619
|
-
} else {
|
|
2620
|
-
console.log(chalk4.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2621
|
-
}
|
|
2622
|
-
console.log(chalk4.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
2623
|
-
console.log(chalk4.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
2624
|
-
console.log();
|
|
2625
|
-
process.exit(1);
|
|
2626
|
-
}
|
|
2627
|
-
if (result.isAuthError) {
|
|
2628
|
-
console.log();
|
|
2629
|
-
console.log(chalk4.red(" \u26A0 Authentication Error"));
|
|
2630
|
-
console.log();
|
|
2631
|
-
console.log(chalk4.dim(` ${result.error}`));
|
|
2632
|
-
console.log();
|
|
2633
|
-
process.exit(1);
|
|
3185
|
+
// src/utils/logging.ts
|
|
3186
|
+
import { appendFile, mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
|
|
3187
|
+
import { dirname as dirname2, join as join8 } from "path";
|
|
3188
|
+
var LOG_FILE_NAME = "quickpeek.log";
|
|
3189
|
+
var logFileInitialized = false;
|
|
3190
|
+
var logFilePath = LOG_FILE_NAME;
|
|
3191
|
+
async function writeLogEntry(entry) {
|
|
3192
|
+
try {
|
|
3193
|
+
if (!logFileInitialized) {
|
|
3194
|
+
await writeFile5(logFilePath, entry);
|
|
3195
|
+
logFileInitialized = true;
|
|
3196
|
+
} else {
|
|
3197
|
+
await appendFile(logFilePath, entry);
|
|
2634
3198
|
}
|
|
2635
|
-
|
|
2636
|
-
console.log(chalk4.red(" \u26A0 AI Error"));
|
|
2637
|
-
console.log();
|
|
2638
|
-
const firstLine = result.error.split("\n")[0] ?? result.error;
|
|
2639
|
-
console.log(chalk4.dim(` ${firstLine.slice(0, 80)}`));
|
|
2640
|
-
console.log();
|
|
2641
|
-
process.exit(1);
|
|
3199
|
+
} catch {
|
|
2642
3200
|
}
|
|
2643
|
-
lastRateLimitInfo = result.limits;
|
|
2644
|
-
return result.content;
|
|
2645
3201
|
}
|
|
3202
|
+
async function setLogFilePath(outputDir) {
|
|
3203
|
+
logFilePath = join8(outputDir, LOG_FILE_NAME);
|
|
3204
|
+
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3205
|
+
${"=".repeat(60)}
|
|
2646
3206
|
|
|
2647
|
-
|
|
2648
|
-
import { mkdir as mkdir4 } from "fs/promises";
|
|
2649
|
-
import { chromium as chromium3 } from "playwright";
|
|
2650
|
-
|
|
2651
|
-
// src/browser/wsl.ts
|
|
2652
|
-
import { execSync, spawn as spawn2 } from "child_process";
|
|
2653
|
-
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
2654
|
-
import { request } from "http";
|
|
2655
|
-
import { chromium as chromium2 } from "playwright";
|
|
2656
|
-
function isWSL() {
|
|
2657
|
-
if (process.platform !== "linux") return false;
|
|
3207
|
+
`;
|
|
2658
3208
|
try {
|
|
2659
|
-
|
|
2660
|
-
|
|
3209
|
+
await mkdir5(dirname2(logFilePath), { recursive: true });
|
|
3210
|
+
await writeFile5(logFilePath, header2);
|
|
3211
|
+
logFileInitialized = true;
|
|
2661
3212
|
} catch {
|
|
2662
|
-
return false;
|
|
2663
3213
|
}
|
|
2664
3214
|
}
|
|
2665
|
-
function
|
|
2666
|
-
|
|
3215
|
+
async function logInfo(context, message) {
|
|
3216
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
|
|
3217
|
+
`);
|
|
2667
3218
|
}
|
|
2668
|
-
function
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
if (!m) return wslPath;
|
|
2672
|
-
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
3219
|
+
async function logDebug(context, message) {
|
|
3220
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
3221
|
+
`);
|
|
2673
3222
|
}
|
|
2674
|
-
function
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
}
|
|
3223
|
+
async function logError(context, error) {
|
|
3224
|
+
let errorMsg;
|
|
3225
|
+
if (error instanceof Error) {
|
|
3226
|
+
errorMsg = `${error.message}
|
|
3227
|
+
${error.stack}`;
|
|
3228
|
+
} else if (typeof error === "object" && error !== null) {
|
|
3229
|
+
errorMsg = JSON.stringify(error, null, 2);
|
|
3230
|
+
} else {
|
|
3231
|
+
errorMsg = String(error);
|
|
2680
3232
|
}
|
|
2681
|
-
|
|
3233
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3234
|
+
${errorMsg}
|
|
3235
|
+
|
|
3236
|
+
`);
|
|
2682
3237
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
3238
|
+
async function logStep(stepNum, action, details) {
|
|
3239
|
+
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3240
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3241
|
+
`);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/ai/mcp-context.ts
|
|
3245
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3246
|
+
var DelegatedGenerationError = class extends Error {
|
|
3247
|
+
tool;
|
|
3248
|
+
step;
|
|
3249
|
+
/** The system instructions the call was about to send. */
|
|
3250
|
+
system;
|
|
3251
|
+
/** The user-side prompt. */
|
|
3252
|
+
prompt;
|
|
3253
|
+
constructor(tool, step, system, prompt2) {
|
|
3254
|
+
super(
|
|
3255
|
+
`Generation for "${tool}" (${step}) belongs to the calling model, not Groq. The tool should hand back this prompt and accept the result as a parameter.`
|
|
2693
3256
|
);
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
if (m?.[1]) return m[1];
|
|
2700
|
-
} catch {
|
|
3257
|
+
this.name = "DelegatedGenerationError";
|
|
3258
|
+
this.tool = tool;
|
|
3259
|
+
this.step = step;
|
|
3260
|
+
this.system = system;
|
|
3261
|
+
this.prompt = prompt2;
|
|
2701
3262
|
}
|
|
2702
|
-
|
|
3263
|
+
};
|
|
3264
|
+
var mcpCall = new AsyncLocalStorage();
|
|
3265
|
+
function runInMcpTool(tool, fn, answers = {}) {
|
|
3266
|
+
return mcpCall.run({ tool, answers, pending: [] }, fn);
|
|
2703
3267
|
}
|
|
2704
|
-
function
|
|
2705
|
-
|
|
2706
|
-
return new Promise((resolve10, reject) => {
|
|
2707
|
-
const proc = spawn2(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2708
|
-
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2709
|
-
let buf = "";
|
|
2710
|
-
proc.stdout.on("data", (d) => {
|
|
2711
|
-
buf += d.toString();
|
|
2712
|
-
const m = buf.match(/PORT:(\d+)/);
|
|
2713
|
-
if (m) {
|
|
2714
|
-
clearTimeout(timer);
|
|
2715
|
-
resolve10({ proc, port: +m[1] });
|
|
2716
|
-
}
|
|
2717
|
-
});
|
|
2718
|
-
proc.on("error", (e) => {
|
|
2719
|
-
clearTimeout(timer);
|
|
2720
|
-
reject(e);
|
|
2721
|
-
});
|
|
2722
|
-
});
|
|
3268
|
+
function currentMcpTool() {
|
|
3269
|
+
return mcpCall.getStore()?.tool ?? null;
|
|
2723
3270
|
}
|
|
2724
|
-
function
|
|
2725
|
-
return
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3271
|
+
function suppliedAnswer(step) {
|
|
3272
|
+
return mcpCall.getStore()?.answers?.[step];
|
|
3273
|
+
}
|
|
3274
|
+
function recordDelegation(e) {
|
|
3275
|
+
const store = mcpCall.getStore();
|
|
3276
|
+
if (store && !store.pending.some((p) => p.step === e.step)) store.pending.push(e);
|
|
3277
|
+
}
|
|
3278
|
+
function pendingDelegations() {
|
|
3279
|
+
return mcpCall.getStore()?.pending ?? [];
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
// src/ai/client.ts
|
|
3283
|
+
var lastRateLimitInfo = null;
|
|
3284
|
+
function wrap(text, width) {
|
|
3285
|
+
const lines = [];
|
|
3286
|
+
for (const paragraph of text.split("\n")) {
|
|
3287
|
+
let current = "";
|
|
3288
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
3289
|
+
if (current && current.length + word.length + 1 > width) {
|
|
3290
|
+
lines.push(current);
|
|
3291
|
+
current = word;
|
|
3292
|
+
} else {
|
|
3293
|
+
current = current ? `${current} ${word}` : word;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
lines.push(current);
|
|
3297
|
+
}
|
|
3298
|
+
return lines.filter((l) => l.length > 0);
|
|
3299
|
+
}
|
|
3300
|
+
function diagnose(status, host) {
|
|
3301
|
+
const where = host ?? "the AI service";
|
|
3302
|
+
if (status === void 0) return `Could not reach ${where}. Check your network, then retry.`;
|
|
3303
|
+
if (status === 401 || status === 403) return `${where} rejected the credentials. Run qp login and retry.`;
|
|
3304
|
+
if (status === 413) return `The prompt was too large for ${where}. Shorten the script or the page.`;
|
|
3305
|
+
if (status === 429) return `${where} is rate limiting. Wait for the quota to reset, then retry.`;
|
|
3306
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
3307
|
+
return `${where} could not reach the AI backend (upstream outage). Retry in a few minutes.`;
|
|
3308
|
+
}
|
|
3309
|
+
if (status >= 500) return `${where} failed internally. Retry in a few minutes; if it persists, report this.`;
|
|
3310
|
+
return `${where} rejected the request. See the body above.`;
|
|
3311
|
+
}
|
|
3312
|
+
function getLastRateLimitInfo() {
|
|
3313
|
+
return lastRateLimitInfo;
|
|
3314
|
+
}
|
|
3315
|
+
function prompt(question) {
|
|
3316
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3317
|
+
return new Promise((resolve10) => {
|
|
3318
|
+
rl.question(question, (answer) => {
|
|
3319
|
+
rl.close();
|
|
3320
|
+
resolve10(answer.trim());
|
|
3321
|
+
});
|
|
2744
3322
|
});
|
|
2745
3323
|
}
|
|
2746
|
-
async function
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
const
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
)
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
return {
|
|
2776
|
-
browser,
|
|
2777
|
-
cleanup: async () => {
|
|
2778
|
-
try {
|
|
2779
|
-
await browser.close();
|
|
2780
|
-
} catch {
|
|
2781
|
-
}
|
|
2782
|
-
relay.kill();
|
|
2783
|
-
}
|
|
2784
|
-
};
|
|
2785
|
-
} catch (err) {
|
|
2786
|
-
relay.kill();
|
|
2787
|
-
throw err;
|
|
3324
|
+
async function setupUserEmail() {
|
|
3325
|
+
console.log();
|
|
3326
|
+
console.log(chalk4.yellow(" \u2709 Account Setup"));
|
|
3327
|
+
console.log(chalk4.dim(" \u2500".repeat(20)));
|
|
3328
|
+
console.log();
|
|
3329
|
+
console.log(chalk4.white(" Sign in with your Google account to track usage"));
|
|
3330
|
+
console.log(chalk4.white(" and unlock Pro features."));
|
|
3331
|
+
const browserEmail = await browserLogin();
|
|
3332
|
+
if (browserEmail) {
|
|
3333
|
+
console.log();
|
|
3334
|
+
console.log(chalk4.green(` \u2713 Signed in as ${browserEmail}`));
|
|
3335
|
+
console.log();
|
|
3336
|
+
return browserEmail;
|
|
3337
|
+
}
|
|
3338
|
+
console.log();
|
|
3339
|
+
console.log(chalk4.dim(" Or enter your email manually:"));
|
|
3340
|
+
const email = await prompt(chalk4.white(" Email: "));
|
|
3341
|
+
if (!email) {
|
|
3342
|
+
console.log();
|
|
3343
|
+
console.log(chalk4.red(" No email provided. Exiting."));
|
|
3344
|
+
process.exit(1);
|
|
3345
|
+
}
|
|
3346
|
+
if (!email.includes("@") || !email.includes(".")) {
|
|
3347
|
+
console.log();
|
|
3348
|
+
console.log(chalk4.yellow(" \u26A0 That doesn't look like a valid email address."));
|
|
3349
|
+
const proceed = await prompt(chalk4.white(" Continue anyway? ") + chalk4.dim("[y/N] "));
|
|
3350
|
+
if (proceed.toLowerCase() !== "y") {
|
|
3351
|
+
process.exit(1);
|
|
3352
|
+
}
|
|
2788
3353
|
}
|
|
3354
|
+
console.log();
|
|
3355
|
+
console.log(chalk4.green(" \u2713 Email registered"));
|
|
3356
|
+
console.log();
|
|
3357
|
+
return email;
|
|
2789
3358
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
3359
|
+
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free", step = "plan") {
|
|
3360
|
+
const mcpTool = currentMcpTool();
|
|
3361
|
+
if (mcpTool) {
|
|
3362
|
+
const answered = suppliedAnswer(step);
|
|
3363
|
+
if (typeof answered === "string") return answered;
|
|
3364
|
+
const refusal = new DelegatedGenerationError(mcpTool, step, systemPrompt, userPrompt);
|
|
3365
|
+
recordDelegation(refusal);
|
|
3366
|
+
throw refusal;
|
|
3367
|
+
}
|
|
3368
|
+
const onRetry = ({ attempt, status, delayMs, host }) => {
|
|
3369
|
+
console.log(
|
|
3370
|
+
chalk4.dim(
|
|
3371
|
+
`
|
|
3372
|
+
${host} returned ${status}; retrying in ${Math.round(delayMs / 1e3)}s (attempt ${attempt + 1} of 3)`
|
|
3373
|
+
)
|
|
3374
|
+
);
|
|
3375
|
+
};
|
|
3376
|
+
const chatOpts = { onRetry, backoffMs: CLI_BACKOFF_MS };
|
|
3377
|
+
const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier, chatOpts) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier, chatOpts);
|
|
3378
|
+
if (!result.success) {
|
|
3379
|
+
await logError2("AI API", new Error(result.detail ? `${result.error}
|
|
3380
|
+
Body: ${result.detail}` : result.error));
|
|
3381
|
+
if (result.isRateLimit) {
|
|
3382
|
+
console.log();
|
|
3383
|
+
console.log(chalk4.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
3384
|
+
console.log();
|
|
3385
|
+
console.log(chalk4.dim(` ${result.error}`));
|
|
3386
|
+
console.log(chalk4.dim(" Options:"));
|
|
3387
|
+
if (result.retryAfterSeconds) {
|
|
3388
|
+
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
3389
|
+
console.log(chalk4.dim(` \u2022 Wait ${minutes} minutes until quota resets`));
|
|
3390
|
+
} else {
|
|
3391
|
+
console.log(chalk4.dim(" \u2022 Wait until tomorrow (limits reset at midnight UTC)"));
|
|
2822
3392
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
viewport,
|
|
2828
|
-
...videoOpts,
|
|
2829
|
-
acceptDownloads: true
|
|
2830
|
-
});
|
|
3393
|
+
console.log(chalk4.dim(" \u2022 Check your usage at: https://sparkbrain.app/dashboard"));
|
|
3394
|
+
console.log(chalk4.dim(" \u2022 Upgrade to a higher tier for more requests"));
|
|
3395
|
+
console.log();
|
|
3396
|
+
process.exit(1);
|
|
2831
3397
|
}
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3398
|
+
if (result.isAuthError) {
|
|
3399
|
+
console.log();
|
|
3400
|
+
console.log(chalk4.red(` \u26A0 Authentication Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
3401
|
+
console.log();
|
|
3402
|
+
for (const line of wrap(result.error, 76)) {
|
|
3403
|
+
console.log(chalk4.dim(` ${line}`));
|
|
3404
|
+
}
|
|
3405
|
+
console.log();
|
|
3406
|
+
console.log(chalk4.dim(` ${diagnose(result.status, result.host)}`));
|
|
3407
|
+
console.log();
|
|
3408
|
+
process.exit(1);
|
|
3409
|
+
}
|
|
3410
|
+
console.log();
|
|
3411
|
+
console.log(chalk4.red(` \u26A0 AI Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
3412
|
+
console.log();
|
|
3413
|
+
for (const line of wrap(result.error, 76)) {
|
|
3414
|
+
console.log(chalk4.dim(` ${line}`));
|
|
3415
|
+
}
|
|
3416
|
+
if (result.detail && !result.error.includes(result.detail)) {
|
|
3417
|
+
console.log();
|
|
3418
|
+
for (const line of wrap(result.detail, 76).slice(0, 6)) {
|
|
3419
|
+
console.log(chalk4.dim(` ${line}`));
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
console.log();
|
|
3423
|
+
console.log(chalk4.dim(` ${diagnose(result.status, result.host)}`));
|
|
3424
|
+
console.log(chalk4.dim(` Full response body: ${logFilePath}`));
|
|
3425
|
+
console.log();
|
|
3426
|
+
process.exit(1);
|
|
2835
3427
|
}
|
|
2836
|
-
|
|
3428
|
+
lastRateLimitInfo = result.limits;
|
|
3429
|
+
return result.content;
|
|
2837
3430
|
}
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3431
|
+
|
|
3432
|
+
// src/ai/json-plan.ts
|
|
3433
|
+
init_lib();
|
|
3434
|
+
init_utils();
|
|
3435
|
+
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
3436
|
+
var PLAN_SHAPE = `The JSON should have: url, title, description, steps array.
|
|
3437
|
+
Each step has: id (number), action, caption, and optionally target, value.`;
|
|
3438
|
+
var jsonFixUserPrompt = (content, shape) => `Fix this malformed JSON. Return ONLY valid JSON, no explanation.
|
|
3439
|
+
${shape}
|
|
3440
|
+
|
|
3441
|
+
Malformed content:
|
|
3442
|
+
${content}`;
|
|
3443
|
+
var noopTask2 = () => ({ succeed: () => {
|
|
3444
|
+
}, fail: () => {
|
|
3445
|
+
} });
|
|
3446
|
+
async function parseAIJsonWithRepair(content, opts) {
|
|
3447
|
+
const { logError: logError2, userEmail, tier = "free", shape = PLAN_SHAPE, context = "JSON Fix" } = opts;
|
|
3448
|
+
const direct = parseAIJson(content);
|
|
3449
|
+
if (direct) return direct.value;
|
|
3450
|
+
const makeTask = opts.task ?? noopTask2;
|
|
3451
|
+
const fixTask = makeTask("Fixing malformed JSON");
|
|
3452
|
+
try {
|
|
3453
|
+
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content, shape), logError2, userEmail, tier);
|
|
3454
|
+
const repaired = parseAIJson(fixed);
|
|
3455
|
+
if (!repaired) throw new Error("No usable JSON in fix");
|
|
3456
|
+
fixTask.succeed("JSON fixed by AI");
|
|
3457
|
+
return repaired.value;
|
|
3458
|
+
} catch (err) {
|
|
3459
|
+
await logError2(context, err);
|
|
3460
|
+
fixTask.fail("Could not fix JSON");
|
|
3461
|
+
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
2844
3462
|
}
|
|
2845
3463
|
}
|
|
2846
3464
|
|
|
2847
3465
|
// src/plan.ts
|
|
2848
3466
|
init_lib();
|
|
3467
|
+
init_json_repair();
|
|
2849
3468
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
2850
3469
|
const browserTask = task2("Launching browser with extension");
|
|
2851
3470
|
const userDataDir = join9(workDir, ".browser-data");
|
|
2852
|
-
await
|
|
3471
|
+
await mkdir6(userDataDir, { recursive: true });
|
|
2853
3472
|
let context;
|
|
2854
3473
|
let chromeSession = null;
|
|
2855
3474
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
@@ -2934,7 +3553,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
2934
3553
|
else if (el.type === "select" || el.type === "select-one") action = "select";
|
|
2935
3554
|
else if (el.type === "checkbox") action = "check";
|
|
2936
3555
|
else if (["text", "email", "password", "number", "search", "url", "tel"].includes(el.type)) action = "type";
|
|
2937
|
-
const selector = el.id ?
|
|
3556
|
+
const selector = el.id ? idSelector(el.id) : el.name ? `[name="${el.name}"]` : `text="${el.text}"`;
|
|
2938
3557
|
return {
|
|
2939
3558
|
type: el.type === "button" || el.type === "link" ? el.type : "input",
|
|
2940
3559
|
text: el.text,
|
|
@@ -2946,43 +3565,19 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
2946
3565
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
2947
3566
|
return { title, elements };
|
|
2948
3567
|
}
|
|
2949
|
-
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
2950
|
-
var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
2951
|
-
The JSON should have: url, title, description, steps array.
|
|
2952
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
2953
|
-
|
|
2954
|
-
Malformed content:
|
|
2955
|
-
${content}`;
|
|
2956
3568
|
async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
|
|
2957
|
-
|
|
2958
|
-
return JSON.parse(content);
|
|
2959
|
-
} catch {
|
|
2960
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
2961
|
-
try {
|
|
2962
|
-
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
|
|
2963
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
2964
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
2965
|
-
const plan = JSON.parse(jsonMatch[0]);
|
|
2966
|
-
fixTask.succeed("JSON fixed by AI");
|
|
2967
|
-
return plan;
|
|
2968
|
-
} catch (err) {
|
|
2969
|
-
await logError2("JSON Fix", err);
|
|
2970
|
-
fixTask.fail("Could not fix JSON");
|
|
2971
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
3569
|
+
return parseAIJsonWithRepair(content, { logError: logError2, task: task2, userEmail, tier });
|
|
2974
3570
|
}
|
|
2975
|
-
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
|
|
2976
|
-
const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
|
|
3571
|
+
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
|
|
3572
|
+
const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
|
|
2977
3573
|
const aiTask = task2("Generating plan with AI");
|
|
2978
3574
|
const langName = getLanguageName(lang);
|
|
2979
3575
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
2980
|
-
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
|
|
3576
|
+
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
|
|
2981
3577
|
try {
|
|
2982
|
-
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
2983
|
-
const
|
|
2984
|
-
if (!
|
|
2985
|
-
const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
|
|
3578
|
+
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier, "plan");
|
|
3579
|
+
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
3580
|
+
if (!Array.isArray(plan.steps)) throw new Error("AI response has no steps array");
|
|
2986
3581
|
if (lang !== void 0) plan.lang = lang;
|
|
2987
3582
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
2988
3583
|
...s,
|
|
@@ -3006,100 +3601,41 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
3006
3601
|
}
|
|
3007
3602
|
const renumberTask = task2("Validating plan structure");
|
|
3008
3603
|
const originalCount = plan.steps.length;
|
|
3009
|
-
const
|
|
3010
|
-
const isValidStep = (s) => {
|
|
3011
|
-
if (!s?.action) return false;
|
|
3012
|
-
if (s.action === "navigate") {
|
|
3013
|
-
if (!s.target) return false;
|
|
3014
|
-
if (!s.target.startsWith("http://") && !s.target.startsWith("https://")) return false;
|
|
3015
|
-
}
|
|
3016
|
-
if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
|
|
3017
|
-
if (!s.target) return false;
|
|
3018
|
-
if (
|
|
3019
|
-
}
|
|
3020
|
-
if (s.action === "upload") {
|
|
3021
|
-
if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
|
|
3022
|
-
}
|
|
3023
|
-
return true;
|
|
3024
|
-
};
|
|
3025
|
-
plan.steps = plan.steps.filter(isValidStep).map(({ duration, ...step }, i) => ({
|
|
3026
|
-
...step,
|
|
3027
|
-
id: i + 1,
|
|
3028
|
-
// Ensure upload steps have a value placeholder
|
|
3029
|
-
...step.action === "upload" && !step.value ? { value: "[file]" } : {}
|
|
3030
|
-
}));
|
|
3031
|
-
if (plan.steps.length === 0) {
|
|
3032
|
-
renumberTask.fail("No valid steps");
|
|
3033
|
-
throw new Error("Plan has no valid steps");
|
|
3034
|
-
}
|
|
3035
|
-
await writeFile6(planPath, JSON.stringify(plan, null, 2));
|
|
3036
|
-
const removed = originalCount - plan.steps.length;
|
|
3037
|
-
renumberTask.succeed(
|
|
3038
|
-
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
3039
|
-
);
|
|
3040
|
-
return plan;
|
|
3041
|
-
}
|
|
3042
|
-
|
|
3043
|
-
// src/commands/phases/auth-phase.ts
|
|
3044
|
-
init_config_loader();
|
|
3045
|
-
|
|
3046
|
-
// src/commands/utils/auth-helpers.ts
|
|
3047
|
-
init_lib();
|
|
3048
|
-
import chalk6 from "chalk";
|
|
3049
|
-
async function handleRegistrationGate() {
|
|
3050
|
-
console.log();
|
|
3051
|
-
console.log(chalk6.yellow(" \u{1F389} You've made 5 free videos!"));
|
|
3052
|
-
console.log(chalk6.white(" Sign in to continue creating videos."));
|
|
3053
|
-
console.log();
|
|
3054
|
-
console.log(chalk6.dim(" Benefits of signing in:"));
|
|
3055
|
-
console.log(chalk6.dim(" \u2022 20 more videos per month"));
|
|
3056
|
-
console.log(chalk6.dim(" \u2022 Keep your video history"));
|
|
3057
|
-
console.log(chalk6.dim(" \u2022 Upgrade to Pro anytime"));
|
|
3058
|
-
let email = await browserLogin();
|
|
3059
|
-
if (!email) {
|
|
3060
|
-
console.log();
|
|
3061
|
-
email = await promptInput("Email: ");
|
|
3062
|
-
}
|
|
3063
|
-
if (!email || !email.includes("@")) {
|
|
3064
|
-
console.log(chalk6.red(" Invalid email. Run qp again to try again."));
|
|
3065
|
-
return null;
|
|
3066
|
-
}
|
|
3067
|
-
const registerTask = task("Sending verification email");
|
|
3068
|
-
const result = await registerFreeUser(email);
|
|
3069
|
-
if (!result.success) {
|
|
3070
|
-
registerTask.fail(result.error || "Registration failed");
|
|
3071
|
-
return null;
|
|
3072
|
-
}
|
|
3073
|
-
if (result.alreadyVerified) {
|
|
3074
|
-
registerTask.succeed("Already verified!");
|
|
3075
|
-
return { email, verified: true };
|
|
3076
|
-
}
|
|
3077
|
-
registerTask.succeed("Check your email!");
|
|
3078
|
-
console.log();
|
|
3079
|
-
console.log(chalk6.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
3080
|
-
console.log(chalk6.white(` ${email}`));
|
|
3081
|
-
console.log();
|
|
3082
|
-
console.log(chalk6.dim(" Click the link in the email, then press Enter to continue..."));
|
|
3083
|
-
await promptInput("");
|
|
3084
|
-
const verifyTask = task("Checking verification");
|
|
3085
|
-
const verified = await isVerified(email);
|
|
3086
|
-
if (verified) {
|
|
3087
|
-
verifyTask.succeed("Email verified!");
|
|
3088
|
-
return { email, verified: true };
|
|
3604
|
+
const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
|
|
3605
|
+
const isValidStep = (s) => {
|
|
3606
|
+
if (!s?.action) return false;
|
|
3607
|
+
if (s.action === "navigate") {
|
|
3608
|
+
if (!s.target) return false;
|
|
3609
|
+
if (!s.target.startsWith("http://") && !s.target.startsWith("https://")) return false;
|
|
3610
|
+
}
|
|
3611
|
+
if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
|
|
3612
|
+
if (!s.target) return false;
|
|
3613
|
+
if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
|
|
3614
|
+
}
|
|
3615
|
+
if (s.action === "upload") {
|
|
3616
|
+
if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
|
|
3617
|
+
}
|
|
3618
|
+
return true;
|
|
3619
|
+
};
|
|
3620
|
+
plan.steps = plan.steps.filter(isValidStep).map(({ duration, ...step }, i) => ({
|
|
3621
|
+
...step,
|
|
3622
|
+
id: i + 1,
|
|
3623
|
+
// Ensure upload steps have a value placeholder
|
|
3624
|
+
...step.action === "upload" && !step.value ? { value: "[file]" } : {}
|
|
3625
|
+
}));
|
|
3626
|
+
if (plan.steps.length === 0) {
|
|
3627
|
+
renumberTask.fail("No valid steps");
|
|
3628
|
+
throw new Error("Plan has no valid steps");
|
|
3089
3629
|
}
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
console.log();
|
|
3097
|
-
return { email, verified: false };
|
|
3630
|
+
await writeFile6(planPath, JSON.stringify(plan, null, 2));
|
|
3631
|
+
const removed = originalCount - plan.steps.length;
|
|
3632
|
+
renumberTask.succeed(
|
|
3633
|
+
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
3634
|
+
);
|
|
3635
|
+
return plan;
|
|
3098
3636
|
}
|
|
3099
3637
|
|
|
3100
3638
|
// src/commands/utils/record-helpers.ts
|
|
3101
|
-
import chalk7 from "chalk";
|
|
3102
|
-
init_lib();
|
|
3103
3639
|
init_config_loader();
|
|
3104
3640
|
init_utils();
|
|
3105
3641
|
var FREE_VIDEO_LIMIT = 5;
|
|
@@ -3124,41 +3660,41 @@ function displayRateLimitInfo() {
|
|
|
3124
3660
|
const percentage = Math.round(limits.used / limits.limit * 100);
|
|
3125
3661
|
if (percentage >= 90) {
|
|
3126
3662
|
console.log();
|
|
3127
|
-
console.log(
|
|
3663
|
+
console.log(chalk6.yellow(` \u26A0 ${remaining} requests remaining (${limits.tier} tier)`));
|
|
3128
3664
|
if (limits.tier === "free") {
|
|
3129
|
-
console.log(
|
|
3665
|
+
console.log(chalk6.dim(` Upgrade: ${getUpgradeUrl()}`));
|
|
3130
3666
|
}
|
|
3131
3667
|
} else if (percentage >= 50) {
|
|
3132
|
-
console.log(
|
|
3668
|
+
console.log(chalk6.dim(` ${remaining}/${limits.limit} requests remaining`));
|
|
3133
3669
|
}
|
|
3134
3670
|
}
|
|
3135
3671
|
async function displayUsageStatus(userEmail, userTier, _localVideoCount = 0) {
|
|
3136
3672
|
const tier = userTier === "pro" ? "pro" : "free";
|
|
3137
|
-
const tierLabel = tier === "pro" ?
|
|
3138
|
-
console.log(
|
|
3139
|
-
console.log(
|
|
3673
|
+
const tierLabel = tier === "pro" ? chalk6.green("Pro") : "Free";
|
|
3674
|
+
console.log(chalk6.cyan(` \u2714 ${tierLabel} plan`));
|
|
3675
|
+
console.log(chalk6.dim(` ${userEmail}`));
|
|
3140
3676
|
if (userEmail && userEmail !== "anonymous") {
|
|
3141
3677
|
try {
|
|
3142
3678
|
const status = await getStatusCached(userEmail);
|
|
3143
3679
|
for (const line of formatUsageLines(status, tier)) {
|
|
3144
|
-
console.log(
|
|
3680
|
+
console.log(chalk6.dim(` ${line}`));
|
|
3145
3681
|
}
|
|
3146
3682
|
} catch {
|
|
3147
3683
|
}
|
|
3148
3684
|
}
|
|
3149
3685
|
if (userTier === "free") {
|
|
3150
3686
|
const upgradeUrl = getUpgradeUrl(userEmail !== "anonymous" ? userEmail : void 0);
|
|
3151
|
-
console.log(
|
|
3687
|
+
console.log(chalk6.dim(` Upgrade: ${upgradeUrl}`));
|
|
3152
3688
|
}
|
|
3153
3689
|
}
|
|
3154
3690
|
function displayPlanSummary(plan) {
|
|
3155
3691
|
console.log();
|
|
3156
|
-
console.log(
|
|
3157
|
-
console.log(
|
|
3692
|
+
console.log(chalk6.dim(` ${plan.title || "Untitled"}`));
|
|
3693
|
+
console.log(chalk6.dim(` ${plan.steps?.length || 0} steps`));
|
|
3158
3694
|
for (const step of plan.steps || []) {
|
|
3159
3695
|
const text = step.caption || step.action || "";
|
|
3160
3696
|
const caption = text.length > 50 ? `${text.slice(0, 47)}...` : text;
|
|
3161
|
-
console.log(
|
|
3697
|
+
console.log(chalk6.dim(` ${step.id}. ${caption}`));
|
|
3162
3698
|
}
|
|
3163
3699
|
}
|
|
3164
3700
|
function getOutputDirFromUrl(url) {
|
|
@@ -3205,6 +3741,86 @@ function getOutputDirFromUrl(url) {
|
|
|
3205
3741
|
}
|
|
3206
3742
|
}
|
|
3207
3743
|
|
|
3744
|
+
// src/commands/edit-cmd.ts
|
|
3745
|
+
var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
3746
|
+
function findPlanFile(dir, lang) {
|
|
3747
|
+
if (lang) {
|
|
3748
|
+
const exact = join10(dir, `plan-${lang}.json`);
|
|
3749
|
+
return existsSync7(exact) ? exact : null;
|
|
3750
|
+
}
|
|
3751
|
+
if (!existsSync7(dir)) return null;
|
|
3752
|
+
const plans = readdirSync(dir).filter((f) => PLAN_FILE.test(f));
|
|
3753
|
+
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
3754
|
+
return preferred ? join10(dir, preferred) : null;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
// src/commands/record-cmd.ts
|
|
3758
|
+
import { existsSync as existsSync18 } from "fs";
|
|
3759
|
+
import { mkdir as mkdir9 } from "fs/promises";
|
|
3760
|
+
import { isAbsolute as isAbsolute5, join as join21, resolve as resolve9 } from "path";
|
|
3761
|
+
import chalk15 from "chalk";
|
|
3762
|
+
init_lib();
|
|
3763
|
+
init_config_loader();
|
|
3764
|
+
|
|
3765
|
+
// src/commands/phases/auth-phase.ts
|
|
3766
|
+
init_lib();
|
|
3767
|
+
import chalk9 from "chalk";
|
|
3768
|
+
init_config_loader();
|
|
3769
|
+
|
|
3770
|
+
// src/commands/utils/auth-helpers.ts
|
|
3771
|
+
init_lib();
|
|
3772
|
+
import chalk8 from "chalk";
|
|
3773
|
+
async function handleRegistrationGate() {
|
|
3774
|
+
console.log();
|
|
3775
|
+
console.log(chalk8.yellow(" \u{1F389} You've made 5 free videos!"));
|
|
3776
|
+
console.log(chalk8.white(" Sign in to continue creating videos."));
|
|
3777
|
+
console.log();
|
|
3778
|
+
console.log(chalk8.dim(" Benefits of signing in:"));
|
|
3779
|
+
console.log(chalk8.dim(" \u2022 20 more videos per month"));
|
|
3780
|
+
console.log(chalk8.dim(" \u2022 Keep your video history"));
|
|
3781
|
+
console.log(chalk8.dim(" \u2022 Upgrade to Pro anytime"));
|
|
3782
|
+
let email = await browserLogin();
|
|
3783
|
+
if (!email) {
|
|
3784
|
+
console.log();
|
|
3785
|
+
email = await promptInput("Email: ");
|
|
3786
|
+
}
|
|
3787
|
+
if (!email || !email.includes("@")) {
|
|
3788
|
+
console.log(chalk8.red(" Invalid email. Run qp again to try again."));
|
|
3789
|
+
return null;
|
|
3790
|
+
}
|
|
3791
|
+
const registerTask = task("Sending verification email");
|
|
3792
|
+
const result = await registerFreeUser(email);
|
|
3793
|
+
if (!result.success) {
|
|
3794
|
+
registerTask.fail(result.error || "Registration failed");
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
if (result.alreadyVerified) {
|
|
3798
|
+
registerTask.succeed("Already verified!");
|
|
3799
|
+
return { email, verified: true };
|
|
3800
|
+
}
|
|
3801
|
+
registerTask.succeed("Check your email!");
|
|
3802
|
+
console.log();
|
|
3803
|
+
console.log(chalk8.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
3804
|
+
console.log(chalk8.white(` ${email}`));
|
|
3805
|
+
console.log();
|
|
3806
|
+
console.log(chalk8.dim(" Click the link in the email, then press Enter to continue..."));
|
|
3807
|
+
await promptInput("");
|
|
3808
|
+
const verifyTask = task("Checking verification");
|
|
3809
|
+
const verified = await isVerified(email);
|
|
3810
|
+
if (verified) {
|
|
3811
|
+
verifyTask.succeed("Email verified!");
|
|
3812
|
+
return { email, verified: true };
|
|
3813
|
+
}
|
|
3814
|
+
verifyTask.fail("Not verified yet");
|
|
3815
|
+
console.log();
|
|
3816
|
+
console.log(chalk8.yellow(" Email not verified. Check your inbox and spam folder."));
|
|
3817
|
+
console.log(chalk8.dim(" Run qp again after clicking the verification link."));
|
|
3818
|
+
console.log();
|
|
3819
|
+
console.log(chalk8.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
|
|
3820
|
+
console.log();
|
|
3821
|
+
return { email, verified: false };
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3208
3824
|
// src/commands/phases/auth-phase.ts
|
|
3209
3825
|
async function runAuthPhase(input) {
|
|
3210
3826
|
let { userEmail, userTier, localVideoCount } = input;
|
|
@@ -3228,7 +3844,7 @@ async function runAuthPhase(input) {
|
|
|
3228
3844
|
} else {
|
|
3229
3845
|
verifyTask.fail("Email not verified");
|
|
3230
3846
|
console.log();
|
|
3231
|
-
console.log(
|
|
3847
|
+
console.log(chalk9.yellow(" Check your inbox for the verification email."));
|
|
3232
3848
|
console.log();
|
|
3233
3849
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
3234
3850
|
if (input2.toLowerCase() === "exit") {
|
|
@@ -3253,8 +3869,8 @@ async function runAuthPhase(input) {
|
|
|
3253
3869
|
registerTask.succeed("Check your email!");
|
|
3254
3870
|
await saveUserData({ email: newEmail, verified: false });
|
|
3255
3871
|
console.log();
|
|
3256
|
-
console.log(
|
|
3257
|
-
console.log(
|
|
3872
|
+
console.log(chalk9.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
|
|
3873
|
+
console.log(chalk9.dim(" Click the link in the email, then run qp again."));
|
|
3258
3874
|
console.log();
|
|
3259
3875
|
gracefulExit();
|
|
3260
3876
|
return null;
|
|
@@ -3270,7 +3886,7 @@ async function runAuthPhase(input) {
|
|
|
3270
3886
|
} else {
|
|
3271
3887
|
recheckTask.fail("Still not verified");
|
|
3272
3888
|
console.log();
|
|
3273
|
-
console.log(
|
|
3889
|
+
console.log(chalk9.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
3274
3890
|
console.log();
|
|
3275
3891
|
gracefulExit();
|
|
3276
3892
|
return null;
|
|
@@ -3297,8 +3913,8 @@ async function runAuthPhase(input) {
|
|
|
3297
3913
|
} else {
|
|
3298
3914
|
const remaining = FREE_VIDEO_LIMIT - localVideoCount;
|
|
3299
3915
|
console.log();
|
|
3300
|
-
console.log(
|
|
3301
|
-
console.log(
|
|
3916
|
+
console.log(chalk9.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
|
|
3917
|
+
console.log(chalk9.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
|
|
3302
3918
|
userEmail = await setupUserEmail();
|
|
3303
3919
|
let registeredVerified = false;
|
|
3304
3920
|
try {
|
|
@@ -3317,18 +3933,18 @@ async function runAuthPhase(input) {
|
|
|
3317
3933
|
}
|
|
3318
3934
|
|
|
3319
3935
|
// src/commands/phases/plan-phase.ts
|
|
3320
|
-
import { existsSync as
|
|
3936
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
|
|
3321
3937
|
import { readFile as readFile4 } from "fs/promises";
|
|
3322
|
-
import { basename, join as
|
|
3323
|
-
import
|
|
3938
|
+
import { basename, join as join11 } from "path";
|
|
3939
|
+
import chalk10 from "chalk";
|
|
3324
3940
|
|
|
3325
3941
|
// src/ai/fix.ts
|
|
3326
3942
|
init_lib();
|
|
3327
3943
|
async function callGroqAndFinalize(systemPrompt, userMessage, plan, logError2, userEmail, tier = "free") {
|
|
3328
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
3329
|
-
const
|
|
3330
|
-
if (!
|
|
3331
|
-
return finalizePlan(
|
|
3944
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
|
|
3945
|
+
const result = await parseAIJsonWithRepair(content, { logError: logError2, userEmail, tier, context: "Plan Fix" });
|
|
3946
|
+
if (!Array.isArray(result.steps)) throw new Error("AI response has no steps array");
|
|
3947
|
+
return finalizePlan(result, plan);
|
|
3332
3948
|
}
|
|
3333
3949
|
function finalizePlan(result, original) {
|
|
3334
3950
|
result.url = original.url;
|
|
@@ -3452,10 +4068,15 @@ async function translatePlan(plan, targetLang, logError2, userEmail, tier = "fre
|
|
|
3452
4068
|
${JSON.stringify(toTranslate)}
|
|
3453
4069
|
|
|
3454
4070
|
Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
3455
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
|
|
4071
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
|
|
4072
|
+
const translated = await parseAIJsonWithRepair(content, {
|
|
4073
|
+
logError: logError2,
|
|
4074
|
+
userEmail,
|
|
4075
|
+
tier,
|
|
4076
|
+
shape: "The JSON should have: description (string), captions (array of strings).",
|
|
4077
|
+
context: "Translate Plan"
|
|
4078
|
+
});
|
|
4079
|
+
if (!Array.isArray(translated.captions)) throw new Error("Translation response has no captions array");
|
|
3459
4080
|
const translatedPlan = JSON.parse(JSON.stringify(plan));
|
|
3460
4081
|
translatedPlan.title = plan.title;
|
|
3461
4082
|
translatedPlan.description = translated.description || plan.description;
|
|
@@ -3473,12 +4094,22 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
|
3473
4094
|
init_config_loader();
|
|
3474
4095
|
var INDENT3 = " ";
|
|
3475
4096
|
async function runPlanPhase(params) {
|
|
3476
|
-
const {
|
|
4097
|
+
const {
|
|
4098
|
+
url,
|
|
4099
|
+
config,
|
|
4100
|
+
userEmail,
|
|
4101
|
+
description,
|
|
4102
|
+
planPath,
|
|
4103
|
+
legacyPlanPath,
|
|
4104
|
+
targetLang,
|
|
4105
|
+
userTier = "free",
|
|
4106
|
+
fresh = false
|
|
4107
|
+
} = params;
|
|
3477
4108
|
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
3478
4109
|
const extensionPath = config.browser?.extension;
|
|
3479
4110
|
let existingPlan = null;
|
|
3480
4111
|
let existingPlanPath = null;
|
|
3481
|
-
if (
|
|
4112
|
+
if (existsSync8(planPath)) {
|
|
3482
4113
|
try {
|
|
3483
4114
|
const parsed = JSON.parse(await readFile4(planPath, "utf-8"));
|
|
3484
4115
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -3490,10 +4121,10 @@ async function runPlanPhase(params) {
|
|
|
3490
4121
|
}
|
|
3491
4122
|
if (!existingPlan) {
|
|
3492
4123
|
try {
|
|
3493
|
-
const files =
|
|
4124
|
+
const files = readdirSync2(config.output.dir);
|
|
3494
4125
|
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== basename(planPath));
|
|
3495
4126
|
for (const file of planFiles) {
|
|
3496
|
-
const filePath =
|
|
4127
|
+
const filePath = join11(config.output.dir, file);
|
|
3497
4128
|
try {
|
|
3498
4129
|
const parsed = JSON.parse(await readFile4(filePath, "utf-8"));
|
|
3499
4130
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -3507,7 +4138,7 @@ async function runPlanPhase(params) {
|
|
|
3507
4138
|
} catch {
|
|
3508
4139
|
}
|
|
3509
4140
|
}
|
|
3510
|
-
if (!existingPlan &&
|
|
4141
|
+
if (!existingPlan && existsSync8(legacyPlanPath)) {
|
|
3511
4142
|
try {
|
|
3512
4143
|
const parsed = JSON.parse(await readFile4(legacyPlanPath, "utf-8"));
|
|
3513
4144
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
@@ -3519,6 +4150,11 @@ async function runPlanPhase(params) {
|
|
|
3519
4150
|
}
|
|
3520
4151
|
const normalizeUrl2 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
3521
4152
|
const urlMatches = existingPlan && normalizeUrl2(existingPlan.url) === normalizeUrl2(url);
|
|
4153
|
+
if (existingPlan && !urlMatches && isAutoYes()) {
|
|
4154
|
+
console.log();
|
|
4155
|
+
console.log(chalk10.dim(` Existing plan is for ${existingPlan.url}, not ${url} \u2014 regenerating.`));
|
|
4156
|
+
existingPlan = null;
|
|
4157
|
+
}
|
|
3522
4158
|
let plan;
|
|
3523
4159
|
const doTranslate = async (failMsg) => {
|
|
3524
4160
|
const translateTask = task(`Translating to ${targetLang}`);
|
|
@@ -3533,25 +4169,25 @@ async function runPlanPhase(params) {
|
|
|
3533
4169
|
translateTask.fail(failMsg);
|
|
3534
4170
|
}
|
|
3535
4171
|
};
|
|
3536
|
-
if (existingPlan && existingPlanPath) {
|
|
4172
|
+
if (existingPlan && existingPlanPath && !fresh) {
|
|
3537
4173
|
console.log();
|
|
3538
|
-
console.log(
|
|
3539
|
-
console.log(
|
|
4174
|
+
console.log(chalk10.dim(` Found existing plan: `) + chalk10.cyan(formatClickablePath(existingPlanPath)));
|
|
4175
|
+
console.log(chalk10.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
|
|
3540
4176
|
if (!urlMatches) {
|
|
3541
|
-
console.log(
|
|
3542
|
-
console.log(
|
|
3543
|
-
console.log(
|
|
4177
|
+
console.log(chalk10.yellow(` \u26A0 Plan URL differs from argument:`));
|
|
4178
|
+
console.log(chalk10.yellow(` Plan: ${existingPlan.url}`));
|
|
4179
|
+
console.log(chalk10.yellow(` Arg: ${url}`));
|
|
3544
4180
|
}
|
|
3545
4181
|
const existingLang = existingPlan.lang || "en";
|
|
3546
4182
|
if (existingLang !== targetLang) {
|
|
3547
|
-
console.log(
|
|
4183
|
+
console.log(chalk10.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
|
|
3548
4184
|
}
|
|
3549
4185
|
const reuse = await confirm(`${INDENT3}Use existing plan?`);
|
|
3550
4186
|
if (reuse) {
|
|
3551
4187
|
plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
|
|
3552
4188
|
if (existingPlanPath !== planPath) {
|
|
3553
4189
|
await savePlan(plan, planPath);
|
|
3554
|
-
console.log(
|
|
4190
|
+
console.log(chalk10.dim(` Migrated to: ${formatClickablePath(planPath)}`));
|
|
3555
4191
|
}
|
|
3556
4192
|
if (existingLang !== targetLang) {
|
|
3557
4193
|
const translate = await confirm(`${INDENT3}Translate to ${targetLang}?`);
|
|
@@ -3569,7 +4205,8 @@ async function runPlanPhase(params) {
|
|
|
3569
4205
|
userEmail,
|
|
3570
4206
|
extensionPath,
|
|
3571
4207
|
config.output.dir,
|
|
3572
|
-
userTier
|
|
4208
|
+
userTier,
|
|
4209
|
+
{ width: config.video.width, height: config.video.height }
|
|
3573
4210
|
);
|
|
3574
4211
|
displayRateLimitInfo();
|
|
3575
4212
|
if (urlMatches) {
|
|
@@ -3586,7 +4223,7 @@ async function runPlanPhase(params) {
|
|
|
3586
4223
|
uploadIdx++;
|
|
3587
4224
|
}
|
|
3588
4225
|
}
|
|
3589
|
-
console.log(
|
|
4226
|
+
console.log(chalk10.dim(` Preserved upload paths from previous plan`));
|
|
3590
4227
|
}
|
|
3591
4228
|
await savePlan(plan, planPath);
|
|
3592
4229
|
plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
|
|
@@ -3602,7 +4239,9 @@ async function runPlanPhase(params) {
|
|
|
3602
4239
|
logError,
|
|
3603
4240
|
userEmail,
|
|
3604
4241
|
extensionPath,
|
|
3605
|
-
config.output.dir
|
|
4242
|
+
config.output.dir,
|
|
4243
|
+
void 0,
|
|
4244
|
+
{ width: config.video.width, height: config.video.height }
|
|
3606
4245
|
);
|
|
3607
4246
|
displayRateLimitInfo();
|
|
3608
4247
|
await savePlan(plan, planPath);
|
|
@@ -3610,8 +4249,8 @@ async function runPlanPhase(params) {
|
|
|
3610
4249
|
}
|
|
3611
4250
|
displayPlanSummary(plan);
|
|
3612
4251
|
console.log();
|
|
3613
|
-
console.log(
|
|
3614
|
-
console.log(
|
|
4252
|
+
console.log(chalk10.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
4253
|
+
console.log(chalk10.dim(` Enter a prompt to refine, or press Enter to continue.`));
|
|
3615
4254
|
const MAX_REFINEMENTS = 10;
|
|
3616
4255
|
let refinementCount = 0;
|
|
3617
4256
|
while (true) {
|
|
@@ -3629,15 +4268,15 @@ async function runPlanPhase(params) {
|
|
|
3629
4268
|
refineTask.succeed("Plan refined");
|
|
3630
4269
|
displayRateLimitInfo();
|
|
3631
4270
|
if (plan.steps.length > maxSteps) {
|
|
3632
|
-
console.log(
|
|
4271
|
+
console.log(chalk10.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
|
|
3633
4272
|
}
|
|
3634
4273
|
displayPlanSummary(plan);
|
|
3635
4274
|
console.log();
|
|
3636
|
-
console.log(
|
|
4275
|
+
console.log(chalk10.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
3637
4276
|
if (refinementCount >= MAX_REFINEMENTS) {
|
|
3638
4277
|
console.log();
|
|
3639
|
-
console.log(
|
|
3640
|
-
console.log(
|
|
4278
|
+
console.log(chalk10.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
|
|
4279
|
+
console.log(chalk10.dim(` Further refinements may incur additional costs.`));
|
|
3641
4280
|
const continueRefining = await confirm(`${INDENT3}Continue refining?`);
|
|
3642
4281
|
if (!continueRefining) break;
|
|
3643
4282
|
}
|
|
@@ -3649,12 +4288,12 @@ async function runPlanPhase(params) {
|
|
|
3649
4288
|
const planLang = plan.lang || "en";
|
|
3650
4289
|
if (planLang !== targetLang) {
|
|
3651
4290
|
let useExistingTranslation = false;
|
|
3652
|
-
if (
|
|
4291
|
+
if (existsSync8(planPath) && existingPlanPath !== planPath) {
|
|
3653
4292
|
try {
|
|
3654
4293
|
const translatedPlan = JSON.parse(await readFile4(planPath, "utf-8"));
|
|
3655
4294
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
3656
4295
|
console.log();
|
|
3657
|
-
console.log(
|
|
4296
|
+
console.log(chalk10.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
3658
4297
|
const useTranslated = await confirm(`${INDENT3}Use ${targetLang} translation?`);
|
|
3659
4298
|
if (useTranslated) {
|
|
3660
4299
|
plan = translatedPlan;
|
|
@@ -3666,7 +4305,7 @@ async function runPlanPhase(params) {
|
|
|
3666
4305
|
}
|
|
3667
4306
|
if (!useExistingTranslation) {
|
|
3668
4307
|
console.log();
|
|
3669
|
-
console.log(
|
|
4308
|
+
console.log(chalk10.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
|
|
3670
4309
|
const translate = await confirm(`${INDENT3}Translate captions to ${targetLang}?`);
|
|
3671
4310
|
if (translate) await doTranslate("Translation failed, using original");
|
|
3672
4311
|
}
|
|
@@ -3676,32 +4315,32 @@ async function runPlanPhase(params) {
|
|
|
3676
4315
|
|
|
3677
4316
|
// src/commands/phases/validation-phase.ts
|
|
3678
4317
|
import { readFile as readFile7 } from "fs/promises";
|
|
3679
|
-
import
|
|
4318
|
+
import chalk12 from "chalk";
|
|
3680
4319
|
|
|
3681
4320
|
// src/record.ts
|
|
3682
|
-
import { existsSync as
|
|
3683
|
-
import { mkdir as
|
|
3684
|
-
import { join as
|
|
3685
|
-
import
|
|
4321
|
+
import { existsSync as existsSync13 } from "fs";
|
|
4322
|
+
import { mkdir as mkdir7 } from "fs/promises";
|
|
4323
|
+
import { join as join16 } from "path";
|
|
4324
|
+
import chalk11 from "chalk";
|
|
3686
4325
|
|
|
3687
4326
|
// src/browser/actions.ts
|
|
3688
|
-
import { existsSync as
|
|
4327
|
+
import { existsSync as existsSync10 } from "fs";
|
|
3689
4328
|
import { readFile as readFile5 } from "fs/promises";
|
|
3690
4329
|
import { isAbsolute as isAbsolute2, resolve as resolve5 } from "path";
|
|
3691
4330
|
|
|
3692
4331
|
// src/browser/effects.ts
|
|
3693
4332
|
init_lib();
|
|
3694
|
-
import { existsSync as
|
|
3695
|
-
import { join as
|
|
4333
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
|
|
4334
|
+
import { join as join12 } from "path";
|
|
3696
4335
|
function loadHighlightCSS() {
|
|
3697
4336
|
const candidates = [
|
|
3698
|
-
|
|
4337
|
+
join12(__dirname, "highlight.css"),
|
|
3699
4338
|
// dist/ build
|
|
3700
|
-
|
|
4339
|
+
join12(__dirname, "..", "highlight.css")
|
|
3701
4340
|
// src/browser/ → src/
|
|
3702
4341
|
];
|
|
3703
4342
|
for (const p of candidates) {
|
|
3704
|
-
if (
|
|
4343
|
+
if (existsSync9(p)) return readFileSync5(p, "utf-8");
|
|
3705
4344
|
}
|
|
3706
4345
|
return "";
|
|
3707
4346
|
}
|
|
@@ -3901,6 +4540,10 @@ async function resolveElement(page, target) {
|
|
|
3901
4540
|
await locator.waitFor({ state: "visible", timeout });
|
|
3902
4541
|
return await locator.elementHandle({ timeout: 1e3 });
|
|
3903
4542
|
}
|
|
4543
|
+
if (target.startsWith("#") && !/^#[A-Za-z_-][\w-]*$/.test(target)) {
|
|
4544
|
+
const attr = `[id="${target.slice(1).replace(/(["\\])/g, "\\$1")}"]`;
|
|
4545
|
+
return await page.waitForSelector(attr, { state: "visible", timeout });
|
|
4546
|
+
}
|
|
3904
4547
|
return await page.waitForSelector(target, { state: "visible", timeout });
|
|
3905
4548
|
} catch {
|
|
3906
4549
|
return null;
|
|
@@ -4025,7 +4668,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4025
4668
|
}
|
|
4026
4669
|
} else {
|
|
4027
4670
|
const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
|
|
4028
|
-
await page.evaluate((y) =>
|
|
4671
|
+
const moved = await page.evaluate((y) => {
|
|
4672
|
+
const before = window.scrollY;
|
|
4673
|
+
window.scrollBy(0, y);
|
|
4674
|
+
return window.scrollY - before;
|
|
4675
|
+
}, amount);
|
|
4676
|
+
if (moved === 0) {
|
|
4677
|
+
throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
|
|
4678
|
+
}
|
|
4029
4679
|
}
|
|
4030
4680
|
break;
|
|
4031
4681
|
}
|
|
@@ -4043,7 +4693,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4043
4693
|
if (!isPlaceholder) {
|
|
4044
4694
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
4045
4695
|
const filePath = isAbsolute2(normalizedPath) ? normalizedPath : resolve5(baseDir, normalizedPath);
|
|
4046
|
-
if (
|
|
4696
|
+
if (existsSync10(filePath)) {
|
|
4047
4697
|
await fileInput.setInputFiles(filePath);
|
|
4048
4698
|
await page.waitForTimeout(2e3);
|
|
4049
4699
|
break;
|
|
@@ -4160,7 +4810,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4160
4810
|
break;
|
|
4161
4811
|
case "image": {
|
|
4162
4812
|
const imagePath = step.value ? isAbsolute2(step.value) ? step.value : resolve5(baseDir, step.value) : null;
|
|
4163
|
-
if (imagePath &&
|
|
4813
|
+
if (imagePath && existsSync10(imagePath)) {
|
|
4164
4814
|
const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
|
|
4165
4815
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
4166
4816
|
}
|
|
@@ -4190,7 +4840,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
4190
4840
|
await pendingDownload;
|
|
4191
4841
|
pendingDownload = null;
|
|
4192
4842
|
}
|
|
4193
|
-
if (lastDownloadPath &&
|
|
4843
|
+
if (lastDownloadPath && existsSync10(lastDownloadPath)) {
|
|
4194
4844
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
4195
4845
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
4196
4846
|
}
|
|
@@ -4235,22 +4885,22 @@ init_lib();
|
|
|
4235
4885
|
|
|
4236
4886
|
// src/browser/dry-run.ts
|
|
4237
4887
|
init_utils();
|
|
4238
|
-
import { join as
|
|
4888
|
+
import { join as join14 } from "path";
|
|
4239
4889
|
|
|
4240
4890
|
// src/browser/step-validator.ts
|
|
4241
4891
|
init_utils();
|
|
4242
|
-
import { existsSync as
|
|
4243
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
4892
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3 } from "fs";
|
|
4893
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join13, resolve as resolve6 } from "path";
|
|
4244
4894
|
function findMediaFile(filename, baseDir) {
|
|
4245
4895
|
const name = basename2(filename);
|
|
4246
|
-
const searchDirs = [baseDir,
|
|
4896
|
+
const searchDirs = [baseDir, join13(baseDir, "res"), join13(baseDir, "assets"), join13(baseDir, "media")];
|
|
4247
4897
|
for (const dir of searchDirs) {
|
|
4248
|
-
if (!
|
|
4898
|
+
if (!existsSync11(dir)) continue;
|
|
4249
4899
|
try {
|
|
4250
|
-
const files =
|
|
4251
|
-
if (files.includes(name)) return
|
|
4900
|
+
const files = readdirSync3(dir);
|
|
4901
|
+
if (files.includes(name)) return join13(dir, name);
|
|
4252
4902
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
4253
|
-
if (match) return
|
|
4903
|
+
if (match) return join13(dir, match);
|
|
4254
4904
|
} catch {
|
|
4255
4905
|
}
|
|
4256
4906
|
}
|
|
@@ -4323,7 +4973,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
4323
4973
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
4324
4974
|
}
|
|
4325
4975
|
const specifiedPath = isAbsolute3(step.value) ? step.value : resolve6(baseDir, step.value);
|
|
4326
|
-
if (
|
|
4976
|
+
if (existsSync11(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
4327
4977
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
4328
4978
|
if (foundPath) {
|
|
4329
4979
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4445,7 +5095,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
4445
5095
|
if (!isPlaceholder) {
|
|
4446
5096
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
4447
5097
|
let filePath = isAbsolute3(normalizedPath) ? normalizedPath : resolve6(baseDir, normalizedPath);
|
|
4448
|
-
if (!
|
|
5098
|
+
if (!existsSync11(filePath)) {
|
|
4449
5099
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
4450
5100
|
if (foundPath) {
|
|
4451
5101
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4544,7 +5194,7 @@ async function getPageElements(page) {
|
|
|
4544
5194
|
});
|
|
4545
5195
|
}
|
|
4546
5196
|
async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart, onStepEnd) {
|
|
4547
|
-
const userDataDir =
|
|
5197
|
+
const userDataDir = join14(baseDir, ".browser-data");
|
|
4548
5198
|
const { context, browser, chromeSession } = await launchBrowserContext(
|
|
4549
5199
|
config,
|
|
4550
5200
|
userDataDir,
|
|
@@ -4609,17 +5259,17 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
4609
5259
|
}
|
|
4610
5260
|
|
|
4611
5261
|
// src/browser/meta.ts
|
|
4612
|
-
import { existsSync as
|
|
5262
|
+
import { existsSync as existsSync12 } from "fs";
|
|
4613
5263
|
import { readFile as readFile6, writeFile as writeFile7 } from "fs/promises";
|
|
4614
|
-
import { join as
|
|
5264
|
+
import { join as join15 } from "path";
|
|
4615
5265
|
var RECORDING_FILE = "recording.mp4";
|
|
4616
5266
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
4617
5267
|
function getRecordingPath(outputDir) {
|
|
4618
|
-
return
|
|
5268
|
+
return join15(outputDir, RECORDING_FILE);
|
|
4619
5269
|
}
|
|
4620
5270
|
async function loadRecordingMeta(outputDir) {
|
|
4621
|
-
const metaPath =
|
|
4622
|
-
if (!
|
|
5271
|
+
const metaPath = join15(outputDir, RECORDING_META_FILE);
|
|
5272
|
+
if (!existsSync12(metaPath)) return null;
|
|
4623
5273
|
try {
|
|
4624
5274
|
const content = await readFile6(metaPath, "utf-8");
|
|
4625
5275
|
return JSON.parse(content);
|
|
@@ -4628,7 +5278,7 @@ async function loadRecordingMeta(outputDir) {
|
|
|
4628
5278
|
}
|
|
4629
5279
|
}
|
|
4630
5280
|
async function saveRecordingMeta(outputDir, meta) {
|
|
4631
|
-
const metaPath =
|
|
5281
|
+
const metaPath = join15(outputDir, RECORDING_META_FILE);
|
|
4632
5282
|
await writeFile7(metaPath, JSON.stringify(meta, null, 2));
|
|
4633
5283
|
}
|
|
4634
5284
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
@@ -4698,15 +5348,15 @@ function formatStepLabel(step) {
|
|
|
4698
5348
|
return `${caption.slice(0, 47)}...`;
|
|
4699
5349
|
}
|
|
4700
5350
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4701
|
-
const videoDir =
|
|
4702
|
-
await
|
|
5351
|
+
const videoDir = join16(workDir, "video");
|
|
5352
|
+
await mkdir7(videoDir, { recursive: true });
|
|
4703
5353
|
const getStepDuration = (stepNum) => {
|
|
4704
5354
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4705
5355
|
};
|
|
4706
5356
|
const browserTask = task2("Starting browser recording");
|
|
4707
5357
|
const recordingStartTime = Date.now();
|
|
4708
|
-
const userDataDir =
|
|
4709
|
-
const recordVideo = { dir: videoDir, size:
|
|
5358
|
+
const userDataDir = join16(workDir, "browser-data");
|
|
5359
|
+
const recordVideo = { dir: videoDir, size: recordingViewport(config) };
|
|
4710
5360
|
const authState = findAuthState(plan.url);
|
|
4711
5361
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
4712
5362
|
const page = context.pages()[0] || await context.newPage();
|
|
@@ -4727,6 +5377,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4727
5377
|
let onMediaPage = firstStepIsMedia;
|
|
4728
5378
|
const failures = [];
|
|
4729
5379
|
const actualStepDurations = [];
|
|
5380
|
+
let pageClosedEarly = false;
|
|
4730
5381
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
4731
5382
|
const step = plan.steps[i];
|
|
4732
5383
|
const stepNum = i + 1;
|
|
@@ -4753,6 +5404,12 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4753
5404
|
let targetEl = null;
|
|
4754
5405
|
if (step.target) {
|
|
4755
5406
|
targetEl = await resolveElement(page, step.target);
|
|
5407
|
+
if (!targetEl) {
|
|
5408
|
+
console.log(
|
|
5409
|
+
`
|
|
5410
|
+
\u26A0 target "${step.target}" not found on page \u2014 step ${stepNum} recorded without the action`
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
4756
5413
|
}
|
|
4757
5414
|
if (step.zoom && step.zoom !== 1 && targetEl && !isExtensionPage) {
|
|
4758
5415
|
const preZoomBox = await targetEl.boundingBox() ?? void 0;
|
|
@@ -4789,11 +5446,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4789
5446
|
}
|
|
4790
5447
|
const stepElapsed = Date.now() - stepStartTime;
|
|
4791
5448
|
const waitTime = Math.max(0, stepDuration - stepElapsed);
|
|
4792
|
-
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
4793
5449
|
const stepDelay = config.video.stepDelay ?? 500;
|
|
4794
5450
|
const isLastStep = i === plan.steps.length - 1;
|
|
4795
|
-
|
|
4796
|
-
await page.waitForTimeout(
|
|
5451
|
+
try {
|
|
5452
|
+
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
5453
|
+
if (!isLastStep && stepDelay > 0) await page.waitForTimeout(stepDelay);
|
|
5454
|
+
} catch (err) {
|
|
5455
|
+
if (page.isClosed()) {
|
|
5456
|
+
console.log(chalk11.yellow("\n \u26A0 Browser page closed early \u2014 saving the recording captured so far"));
|
|
5457
|
+
pageClosedEarly = true;
|
|
5458
|
+
actualStepDurations.push(Math.max(stepElapsed, stepDuration));
|
|
5459
|
+
break;
|
|
5460
|
+
}
|
|
5461
|
+
throw err;
|
|
4797
5462
|
}
|
|
4798
5463
|
if (!isExtensionPage) {
|
|
4799
5464
|
const shouldHighlight = step.highlight !== void 0 ? step.highlight : config.video.highlight;
|
|
@@ -4806,18 +5471,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4806
5471
|
actualStepDurations.push(actualDuration);
|
|
4807
5472
|
}
|
|
4808
5473
|
if (failures.length > 0) {
|
|
4809
|
-
console.log(
|
|
5474
|
+
console.log(chalk11.yellow(` \u26A0 ${failures.length} step(s) skipped:`));
|
|
4810
5475
|
for (const f of failures) {
|
|
4811
|
-
console.log(
|
|
5476
|
+
console.log(chalk11.yellow(` - ${f}`));
|
|
4812
5477
|
}
|
|
4813
5478
|
}
|
|
5479
|
+
while (actualStepDurations.length < plan.steps.length) actualStepDurations.push(0);
|
|
4814
5480
|
const saveTask = task2("Saving video");
|
|
4815
5481
|
const video = page.video();
|
|
4816
5482
|
if (!video) throw new Error("No video recorder available");
|
|
4817
|
-
await page.close();
|
|
5483
|
+
if (!pageClosedEarly) await page.close();
|
|
4818
5484
|
const videoPath = await video.path();
|
|
4819
5485
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4820
|
-
if (!videoPath || !
|
|
5486
|
+
if (!videoPath || !existsSync13(videoPath)) {
|
|
4821
5487
|
throw new Error("Video file was not created");
|
|
4822
5488
|
}
|
|
4823
5489
|
saveTask.succeed("Video saved");
|
|
@@ -4876,21 +5542,21 @@ async function runValidationPhase(params) {
|
|
|
4876
5542
|
const stepNum = result.stepIndex + 1;
|
|
4877
5543
|
const step = plan.steps[result.stepIndex];
|
|
4878
5544
|
console.log();
|
|
4879
|
-
console.log(
|
|
5545
|
+
console.log(chalk12.red(` \u2717 Step ${stepNum}: ${step.action}`));
|
|
4880
5546
|
const caption = step.caption || "";
|
|
4881
|
-
if (caption) console.log(
|
|
4882
|
-
console.log(
|
|
4883
|
-
if (step.value) console.log(
|
|
4884
|
-
console.log(
|
|
5547
|
+
if (caption) console.log(chalk12.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
|
|
5548
|
+
console.log(chalk12.dim(` Target: ${step.target || "none"}`));
|
|
5549
|
+
if (step.value) console.log(chalk12.dim(` Value: ${step.value}`));
|
|
5550
|
+
console.log(chalk12.dim(` Error: ${result.error}`));
|
|
4885
5551
|
if (result.suggestion) {
|
|
4886
|
-
console.log(
|
|
5552
|
+
console.log(chalk12.yellow(` Hint: ${result.suggestion}`));
|
|
4887
5553
|
}
|
|
4888
5554
|
console.log();
|
|
4889
5555
|
const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
|
|
4890
5556
|
if (isUploadMissingFile) {
|
|
4891
5557
|
const filePath = await promptInput('Enter file path to upload (or "skip"): ');
|
|
4892
5558
|
if (filePath.toLowerCase() === "skip") {
|
|
4893
|
-
console.log(
|
|
5559
|
+
console.log(chalk12.yellow(` Skipping step ${stepNum}`));
|
|
4894
5560
|
continue;
|
|
4895
5561
|
}
|
|
4896
5562
|
if (filePath) {
|
|
@@ -4899,13 +5565,13 @@ async function runValidationPhase(params) {
|
|
|
4899
5565
|
const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
4900
5566
|
const newResult2 = revalidation2.results[result.stepIndex];
|
|
4901
5567
|
if (newResult2?.success) {
|
|
4902
|
-
console.log(
|
|
5568
|
+
console.log(chalk12.green(` \u2713 Step ${stepNum} now passes`));
|
|
4903
5569
|
validationResult = revalidation2;
|
|
4904
5570
|
continue;
|
|
4905
5571
|
}
|
|
4906
5572
|
result.error = newResult2?.error;
|
|
4907
5573
|
result.suggestion = newResult2?.suggestion;
|
|
4908
|
-
console.log(
|
|
5574
|
+
console.log(chalk12.red(` Still failing: ${result.error}`));
|
|
4909
5575
|
}
|
|
4910
5576
|
}
|
|
4911
5577
|
const fixTask = task(`Fixing step ${stepNum} with AI`);
|
|
@@ -4927,8 +5593,8 @@ async function runValidationPhase(params) {
|
|
|
4927
5593
|
await savePlan(plan, planPath);
|
|
4928
5594
|
fixTask.succeed("AI suggested a fix");
|
|
4929
5595
|
const fixedStep = plan.steps[result.stepIndex];
|
|
4930
|
-
console.log(
|
|
4931
|
-
if (fixedStep.value) console.log(
|
|
5596
|
+
console.log(chalk12.dim(` New target: ${fixedStep.target || "none"}`));
|
|
5597
|
+
if (fixedStep.value) console.log(chalk12.dim(` New value: ${fixedStep.value}`));
|
|
4932
5598
|
} catch {
|
|
4933
5599
|
fixTask.fail("AI could not fix automatically");
|
|
4934
5600
|
}
|
|
@@ -4944,11 +5610,11 @@ async function runValidationPhase(params) {
|
|
|
4944
5610
|
result.error = newResult?.error;
|
|
4945
5611
|
result.suggestion = newResult?.suggestion;
|
|
4946
5612
|
console.log();
|
|
4947
|
-
console.log(
|
|
5613
|
+
console.log(chalk12.yellow(` Edit step ${stepNum} in: `) + chalk12.cyan(formatClickablePath(planPath)));
|
|
4948
5614
|
console.log();
|
|
4949
5615
|
const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
|
|
4950
5616
|
if (choice.toLowerCase() === "skip") {
|
|
4951
|
-
console.log(
|
|
5617
|
+
console.log(chalk12.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
4952
5618
|
continue;
|
|
4953
5619
|
}
|
|
4954
5620
|
plan = JSON.parse(await readFile7(planPath, "utf-8"));
|
|
@@ -4976,17 +5642,17 @@ async function runValidationPhase(params) {
|
|
|
4976
5642
|
|
|
4977
5643
|
// src/commands/phases/video-phase.ts
|
|
4978
5644
|
init_lib();
|
|
4979
|
-
import { existsSync as
|
|
5645
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4980
5646
|
import { copyFile } from "fs/promises";
|
|
4981
|
-
import { join as
|
|
5647
|
+
import { join as join18, resolve as resolve8 } from "path";
|
|
4982
5648
|
|
|
4983
5649
|
// src/utils/video-pipeline.ts
|
|
4984
5650
|
init_lib();
|
|
4985
5651
|
import { chunkWordsByCount, assTimeFromMs as formatASSTime } from "@spark-apps/video-kit";
|
|
4986
|
-
import { existsSync as
|
|
4987
|
-
import { mkdir as
|
|
4988
|
-
import { isAbsolute as isAbsolute4, join as
|
|
4989
|
-
import
|
|
5652
|
+
import { existsSync as existsSync14 } from "fs";
|
|
5653
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
5654
|
+
import { isAbsolute as isAbsolute4, join as join17, resolve as resolve7 } from "path";
|
|
5655
|
+
import chalk13 from "chalk";
|
|
4990
5656
|
var INDENT4 = " ";
|
|
4991
5657
|
async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
4992
5658
|
const regularColor = hexToAss(captions.regular.color);
|
|
@@ -5033,7 +5699,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
5033
5699
|
}
|
|
5034
5700
|
currentTime += audio.duration;
|
|
5035
5701
|
}
|
|
5036
|
-
const assPath =
|
|
5702
|
+
const assPath = join17(workDir, "subtitles.ass");
|
|
5037
5703
|
await writeFile8(assPath, ass);
|
|
5038
5704
|
return assPath;
|
|
5039
5705
|
}
|
|
@@ -5048,7 +5714,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
5048
5714
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
5049
5715
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
5050
5716
|
const mediaPath = isAbsolute4(step.value) ? step.value : resolve7(baseDir, step.value);
|
|
5051
|
-
if (
|
|
5717
|
+
if (existsSync14(mediaPath)) {
|
|
5052
5718
|
const partial = parsePartial(step.partial);
|
|
5053
5719
|
overlays.push({
|
|
5054
5720
|
path: mediaPath,
|
|
@@ -5067,23 +5733,23 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
5067
5733
|
}
|
|
5068
5734
|
async function composeAndFinish(videoPath, audioPath, assPath, config, userTier = "free", mainContentDuration, trimStart = 0.5, stepTransitions, _stepSync) {
|
|
5069
5735
|
header("Compose");
|
|
5070
|
-
const configPath =
|
|
5071
|
-
console.log(`${INDENT4}${
|
|
5736
|
+
const configPath = join17(process.cwd(), CONFIG_FILE);
|
|
5737
|
+
console.log(`${INDENT4}${chalk13.dim(`Config: ${formatClickablePath(configPath)}`)}`);
|
|
5072
5738
|
const proceed = await confirm(`${INDENT4}Review config or continue?`);
|
|
5073
5739
|
if (!proceed) {
|
|
5074
|
-
console.log(
|
|
5740
|
+
console.log(chalk13.yellow(`
|
|
5075
5741
|
Edit ${CONFIG_FILE} and run again.
|
|
5076
5742
|
`));
|
|
5077
5743
|
process.exit(0);
|
|
5078
5744
|
}
|
|
5079
5745
|
const composeTask = task("Building final video");
|
|
5080
|
-
await
|
|
5746
|
+
await mkdir8(config.output.dir, { recursive: true });
|
|
5081
5747
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
5082
|
-
const finalPath =
|
|
5748
|
+
const finalPath = join17(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
5083
5749
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
5084
|
-
const hasVoiceover = audioPath &&
|
|
5750
|
+
const hasVoiceover = audioPath && existsSync14(audioPath);
|
|
5085
5751
|
const musicPath = config.music?.path ? isAbsolute4(config.music.path) ? config.music.path : resolve7(config.output.dir, config.music.path) : "";
|
|
5086
|
-
const hasMusic = musicPath &&
|
|
5752
|
+
const hasMusic = musicPath && existsSync14(musicPath);
|
|
5087
5753
|
try {
|
|
5088
5754
|
await composeVideo({
|
|
5089
5755
|
videoPath,
|
|
@@ -5122,7 +5788,7 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
|
5122
5788
|
if (duration <= limitSecs * 1e3) return finalPath;
|
|
5123
5789
|
const speedFactor = Math.ceil(duration / (limitSecs * 1e3) * 10) / 10;
|
|
5124
5790
|
console.log();
|
|
5125
|
-
console.log(
|
|
5791
|
+
console.log(chalk13.yellow(` Video is ${(duration / 1e3).toFixed(0)}s - over ${limitSecs}s`));
|
|
5126
5792
|
const speedUp = await confirm(` Speed up ${speedFactor}x to fit under ${limitSecs}s?`);
|
|
5127
5793
|
if (!speedUp) return finalPath;
|
|
5128
5794
|
const speedTask = task(`Speeding up ${speedFactor}x`);
|
|
@@ -5145,6 +5811,21 @@ async function speedUpToFitLimit(finalPath, config, limitSecs = 60) {
|
|
|
5145
5811
|
|
|
5146
5812
|
// src/commands/phases/video-phase.ts
|
|
5147
5813
|
var INDENT5 = " ";
|
|
5814
|
+
function resolveOutro(config, userTier) {
|
|
5815
|
+
if (userTier !== "free") {
|
|
5816
|
+
const custom = config.video.outro?.trim();
|
|
5817
|
+
if (!custom) return void 0;
|
|
5818
|
+
const path = resolve8(custom);
|
|
5819
|
+
if (existsSync15(path)) return path;
|
|
5820
|
+
info(`${INDENT5}\u26A0 outro not found: ${path}`);
|
|
5821
|
+
return void 0;
|
|
5822
|
+
}
|
|
5823
|
+
const scriptDir = getScriptDir();
|
|
5824
|
+
const bundled = join18(scriptDir, "assets", "qp.wm");
|
|
5825
|
+
if (existsSync15(bundled)) return bundled;
|
|
5826
|
+
const fallback = join18(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
5827
|
+
return existsSync15(fallback) ? fallback : void 0;
|
|
5828
|
+
}
|
|
5148
5829
|
async function runVideoPhase(params) {
|
|
5149
5830
|
let { stepAudio } = params;
|
|
5150
5831
|
const { plan, config, voiceoverPath, workDir, captions, userTier } = params;
|
|
@@ -5163,7 +5844,7 @@ async function runVideoPhase(params) {
|
|
|
5163
5844
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
5164
5845
|
} else {
|
|
5165
5846
|
const mediaPath = resolve8(s.value);
|
|
5166
|
-
if (
|
|
5847
|
+
if (existsSync15(mediaPath)) {
|
|
5167
5848
|
try {
|
|
5168
5849
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
5169
5850
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -5175,7 +5856,7 @@ async function runVideoPhase(params) {
|
|
|
5175
5856
|
}
|
|
5176
5857
|
} else {
|
|
5177
5858
|
const mediaPath = resolve8(s.value);
|
|
5178
|
-
if (
|
|
5859
|
+
if (existsSync15(mediaPath)) {
|
|
5179
5860
|
try {
|
|
5180
5861
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
5181
5862
|
} catch {
|
|
@@ -5188,7 +5869,7 @@ async function runVideoPhase(params) {
|
|
|
5188
5869
|
})
|
|
5189
5870
|
);
|
|
5190
5871
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
5191
|
-
const hasExistingRecording = existingRecordingMeta &&
|
|
5872
|
+
const hasExistingRecording = existingRecordingMeta && existsSync15(recordingPath);
|
|
5192
5873
|
let videoPath = recordingPath;
|
|
5193
5874
|
const firstStep = plan.steps[0];
|
|
5194
5875
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -5228,7 +5909,7 @@ async function runVideoPhase(params) {
|
|
|
5228
5909
|
if (videoOverlays.length > 0) {
|
|
5229
5910
|
const spliceTask = task("Splicing videos into recording");
|
|
5230
5911
|
try {
|
|
5231
|
-
const splicedPath =
|
|
5912
|
+
const splicedPath = join18(workDir, "spliced.mp4");
|
|
5232
5913
|
await spliceVideos({
|
|
5233
5914
|
inputPath: videoPath,
|
|
5234
5915
|
outputPath: splicedPath,
|
|
@@ -5248,8 +5929,8 @@ async function runVideoPhase(params) {
|
|
|
5248
5929
|
if (stepTransitions.length > 0) {
|
|
5249
5930
|
const transitionTask = task("Applying transitions");
|
|
5250
5931
|
try {
|
|
5251
|
-
const transitionedPath =
|
|
5252
|
-
const videoDuration =
|
|
5932
|
+
const transitionedPath = join18(workDir, "transitioned.mp4");
|
|
5933
|
+
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
5253
5934
|
await applyTransitions({
|
|
5254
5935
|
inputVideo: videoPath,
|
|
5255
5936
|
outputVideo: transitionedPath,
|
|
@@ -5267,30 +5948,24 @@ async function runVideoPhase(params) {
|
|
|
5267
5948
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5268
5949
|
let videoToCompose = videoPath;
|
|
5269
5950
|
let mainContentDuration;
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
});
|
|
5289
|
-
videoToCompose = combinedPath;
|
|
5290
|
-
} catch (err) {
|
|
5291
|
-
await logError("Concat Outro", err);
|
|
5292
|
-
mainContentDuration = void 0;
|
|
5293
|
-
}
|
|
5951
|
+
const outroPath = resolveOutro(config, userTier);
|
|
5952
|
+
if (outroPath) {
|
|
5953
|
+
try {
|
|
5954
|
+
const mainDuration = await getMediaDuration(videoPath);
|
|
5955
|
+
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
5956
|
+
const combinedPath = join18(workDir, "combined-with-outro.mp4");
|
|
5957
|
+
await concatVideoWithOutro({
|
|
5958
|
+
inputPath: videoPath,
|
|
5959
|
+
outroPath,
|
|
5960
|
+
outputPath: combinedPath,
|
|
5961
|
+
width: config.video.width,
|
|
5962
|
+
height: config.video.height,
|
|
5963
|
+
preset: config.video.preset
|
|
5964
|
+
});
|
|
5965
|
+
videoToCompose = combinedPath;
|
|
5966
|
+
} catch (err) {
|
|
5967
|
+
await logError("Concat Outro", err);
|
|
5968
|
+
mainContentDuration = void 0;
|
|
5294
5969
|
}
|
|
5295
5970
|
}
|
|
5296
5971
|
let finalPath = await composeAndFinish(
|
|
@@ -5325,18 +6000,18 @@ async function runVideoPhase(params) {
|
|
|
5325
6000
|
|
|
5326
6001
|
// src/commands/phases/voiceover-phase.ts
|
|
5327
6002
|
init_lib();
|
|
5328
|
-
import { existsSync as
|
|
5329
|
-
import { join as
|
|
5330
|
-
import
|
|
6003
|
+
import { existsSync as existsSync16 } from "fs";
|
|
6004
|
+
import { join as join19 } from "path";
|
|
6005
|
+
import chalk14 from "chalk";
|
|
5331
6006
|
var INDENT6 = " ";
|
|
5332
6007
|
async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
5333
6008
|
if (fresh) {
|
|
5334
6009
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
5335
6010
|
for (const file of filesToDelete) {
|
|
5336
|
-
const filePath =
|
|
5337
|
-
if (
|
|
5338
|
-
const { unlink:
|
|
5339
|
-
await
|
|
6011
|
+
const filePath = join19(config.output.dir, file);
|
|
6012
|
+
if (existsSync16(filePath)) {
|
|
6013
|
+
const { unlink: unlink4 } = await import("fs/promises");
|
|
6014
|
+
await unlink4(filePath).catch(() => {
|
|
5340
6015
|
});
|
|
5341
6016
|
}
|
|
5342
6017
|
}
|
|
@@ -5344,8 +6019,13 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5344
6019
|
}
|
|
5345
6020
|
const voice = resolveVoice(config);
|
|
5346
6021
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5347
|
-
const voiceoverFile =
|
|
5348
|
-
const
|
|
6022
|
+
const voiceoverFile = join19(config.output.dir, "voiceover.wav");
|
|
6023
|
+
const voiceoverOnDisk = existingMeta && existsSync16(voiceoverFile);
|
|
6024
|
+
const staleContainer = voiceoverOnDisk ? !await isHonestWav(voiceoverFile) : false;
|
|
6025
|
+
if (staleContainer) {
|
|
6026
|
+
info("Existing voiceover has a stale container, regenerating");
|
|
6027
|
+
}
|
|
6028
|
+
const hasExistingVoiceover = voiceoverOnDisk && !staleContainer;
|
|
5349
6029
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5350
6030
|
let voiceoverPath;
|
|
5351
6031
|
let stepAudio;
|
|
@@ -5368,9 +6048,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5368
6048
|
const reuseCheck = canReuseVoiceover(plan.steps, voice, existingMeta);
|
|
5369
6049
|
if (reuseCheck.canReuse) {
|
|
5370
6050
|
console.log();
|
|
5371
|
-
console.log(
|
|
6051
|
+
console.log(chalk14.dim(` Found existing voiceover in ${config.output.dir}`));
|
|
5372
6052
|
if (reuseCheck.reason) {
|
|
5373
|
-
console.log(
|
|
6053
|
+
console.log(chalk14.dim(` ${reuseCheck.reason}`));
|
|
5374
6054
|
}
|
|
5375
6055
|
const reuse = await confirm(`${INDENT6}Reuse existing voiceover?`);
|
|
5376
6056
|
if (reuse) {
|
|
@@ -5402,31 +6082,31 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5402
6082
|
}
|
|
5403
6083
|
|
|
5404
6084
|
// src/commands/utils/chrome-extension.ts
|
|
5405
|
-
import { existsSync as
|
|
6085
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
5406
6086
|
import { homedir as homedir3 } from "os";
|
|
5407
|
-
import { join as
|
|
6087
|
+
import { join as join20 } from "path";
|
|
5408
6088
|
function resolveChromeExtension(extensionId) {
|
|
5409
6089
|
const platform = process.platform;
|
|
5410
6090
|
const possibleDirs = [];
|
|
5411
6091
|
const getChromeExtDirs = (userDataPath) => {
|
|
5412
6092
|
const dirs = [];
|
|
5413
6093
|
try {
|
|
5414
|
-
const profiles =
|
|
6094
|
+
const profiles = readdirSync4(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
5415
6095
|
for (const profile of profiles) {
|
|
5416
|
-
dirs.push(
|
|
6096
|
+
dirs.push(join20(userDataPath, profile, "Extensions"));
|
|
5417
6097
|
}
|
|
5418
6098
|
} catch {
|
|
5419
|
-
dirs.push(
|
|
6099
|
+
dirs.push(join20(userDataPath, "Default", "Extensions"));
|
|
5420
6100
|
}
|
|
5421
6101
|
return dirs;
|
|
5422
6102
|
};
|
|
5423
6103
|
if (platform === "win32") {
|
|
5424
|
-
possibleDirs.push(...getChromeExtDirs(
|
|
6104
|
+
possibleDirs.push(...getChromeExtDirs(join20(homedir3(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
5425
6105
|
} else if (platform === "darwin") {
|
|
5426
|
-
possibleDirs.push(...getChromeExtDirs(
|
|
6106
|
+
possibleDirs.push(...getChromeExtDirs(join20(homedir3(), "Library", "Application Support", "Google", "Chrome")));
|
|
5427
6107
|
} else {
|
|
5428
6108
|
try {
|
|
5429
|
-
const windowsUsers =
|
|
6109
|
+
const windowsUsers = readdirSync4("/mnt/c/Users").filter(
|
|
5430
6110
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
5431
6111
|
);
|
|
5432
6112
|
for (const user of windowsUsers) {
|
|
@@ -5434,22 +6114,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
5434
6114
|
}
|
|
5435
6115
|
} catch {
|
|
5436
6116
|
}
|
|
5437
|
-
possibleDirs.push(...getChromeExtDirs(
|
|
6117
|
+
possibleDirs.push(...getChromeExtDirs(join20(homedir3(), ".config", "google-chrome")));
|
|
5438
6118
|
}
|
|
5439
6119
|
for (const chromeExtDir of possibleDirs) {
|
|
5440
|
-
const extDir =
|
|
5441
|
-
if (!
|
|
6120
|
+
const extDir = join20(chromeExtDir, extensionId);
|
|
6121
|
+
if (!existsSync17(extDir)) continue;
|
|
5442
6122
|
try {
|
|
5443
|
-
const versions =
|
|
6123
|
+
const versions = readdirSync4(extDir).filter((f) => !f.startsWith("."));
|
|
5444
6124
|
if (versions.length === 0) continue;
|
|
5445
6125
|
const latestVersion = versions.sort().pop();
|
|
5446
|
-
const extPath =
|
|
6126
|
+
const extPath = join20(extDir, latestVersion);
|
|
5447
6127
|
let name = extensionId;
|
|
5448
6128
|
let popupPath = "popup.html";
|
|
5449
|
-
const manifestPath =
|
|
5450
|
-
if (
|
|
6129
|
+
const manifestPath = join20(extPath, "manifest.json");
|
|
6130
|
+
if (existsSync17(manifestPath)) {
|
|
5451
6131
|
try {
|
|
5452
|
-
const manifest = JSON.parse(
|
|
6132
|
+
const manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
|
|
5453
6133
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
5454
6134
|
name = manifest.name;
|
|
5455
6135
|
}
|
|
@@ -5480,16 +6160,23 @@ async function recordCommand(url, options = {}) {
|
|
|
5480
6160
|
const extensionId = url.slice(7);
|
|
5481
6161
|
chromeExtension = resolveChromeExtension(extensionId);
|
|
5482
6162
|
if (!chromeExtension) {
|
|
5483
|
-
console.log(
|
|
6163
|
+
console.log(chalk15.red(`
|
|
5484
6164
|
\u2717 Chrome extension not found: ${extensionId}`));
|
|
5485
|
-
console.log(
|
|
6165
|
+
console.log(chalk15.dim(` Make sure the extension is installed in Chrome
|
|
5486
6166
|
`));
|
|
5487
6167
|
process.exit(1);
|
|
5488
6168
|
}
|
|
5489
6169
|
config.browser = { extension: chromeExtension.path };
|
|
5490
6170
|
url = chromeExtension.popupUrl;
|
|
5491
|
-
console.log(
|
|
5492
|
-
console.log(
|
|
6171
|
+
console.log(chalk15.dim(` Extension: ${chromeExtension.name}`));
|
|
6172
|
+
console.log(chalk15.dim(` Path: ${chromeExtension.path}`));
|
|
6173
|
+
}
|
|
6174
|
+
if (!chromeExtension) {
|
|
6175
|
+
const startUrl = resolveStartUrl(url);
|
|
6176
|
+
if (startUrl !== url) {
|
|
6177
|
+
console.log(chalk15.dim(` Signed-in start page: ${startUrl}`));
|
|
6178
|
+
url = startUrl;
|
|
6179
|
+
}
|
|
5493
6180
|
}
|
|
5494
6181
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
5495
6182
|
const userData = await loadUserData();
|
|
@@ -5502,20 +6189,20 @@ async function recordCommand(url, options = {}) {
|
|
|
5502
6189
|
if (!authResult) return;
|
|
5503
6190
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
5504
6191
|
if (configCreated) {
|
|
5505
|
-
const configPath =
|
|
6192
|
+
const configPath = join21(process.cwd(), CONFIG_FILE);
|
|
5506
6193
|
console.log();
|
|
5507
|
-
console.log(
|
|
5508
|
-
console.log(
|
|
6194
|
+
console.log(chalk15.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
6195
|
+
console.log(chalk15.dim(` ${configPath}`));
|
|
5509
6196
|
console.log();
|
|
5510
|
-
console.log(
|
|
5511
|
-
console.log(
|
|
5512
|
-
console.log(
|
|
5513
|
-
console.log(
|
|
5514
|
-
console.log(
|
|
6197
|
+
console.log(chalk15.cyan(" Review the config file to customize:"));
|
|
6198
|
+
console.log(chalk15.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
|
|
6199
|
+
console.log(chalk15.dim(" \u2022 video: Resolution, quality, effects"));
|
|
6200
|
+
console.log(chalk15.dim(" \u2022 music: Background music path and volume"));
|
|
6201
|
+
console.log(chalk15.dim(" \u2022 output: Output directory"));
|
|
5515
6202
|
console.log();
|
|
5516
6203
|
const proceed = await confirm(`${INDENT2}Proceed with default settings?`);
|
|
5517
6204
|
if (!proceed) {
|
|
5518
|
-
console.log(
|
|
6205
|
+
console.log(chalk15.yellow(`
|
|
5519
6206
|
Edit ${CONFIG_FILE} and run again.
|
|
5520
6207
|
`));
|
|
5521
6208
|
process.exit(0);
|
|
@@ -5524,16 +6211,16 @@ async function recordCommand(url, options = {}) {
|
|
|
5524
6211
|
if (langOverride) config.lang = langOverride;
|
|
5525
6212
|
if (config.music?.path) {
|
|
5526
6213
|
const musicPath = isAbsolute5(config.music.path) ? config.music.path : resolve9(config.output.dir, config.music.path);
|
|
5527
|
-
if (!
|
|
6214
|
+
if (!existsSync18(musicPath)) {
|
|
5528
6215
|
console.log();
|
|
5529
|
-
console.log(
|
|
5530
|
-
console.log(
|
|
6216
|
+
console.log(chalk15.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
6217
|
+
console.log(chalk15.dim(` Config: ${join21(process.cwd(), CONFIG_FILE)}`));
|
|
5531
6218
|
console.log();
|
|
5532
6219
|
const proceed = await confirm(`${INDENT2}Continue without background music?`);
|
|
5533
6220
|
if (!proceed) {
|
|
5534
|
-
console.log(
|
|
6221
|
+
console.log(chalk15.yellow(`
|
|
5535
6222
|
Edit music.path in config and run again.`));
|
|
5536
|
-
console.log(
|
|
6223
|
+
console.log(chalk15.dim(` ${join21(process.cwd(), CONFIG_FILE)}
|
|
5537
6224
|
`));
|
|
5538
6225
|
process.exit(0);
|
|
5539
6226
|
}
|
|
@@ -5543,10 +6230,10 @@ async function recordCommand(url, options = {}) {
|
|
|
5543
6230
|
const captions = await loadCaptions();
|
|
5544
6231
|
const workDir = await prepareWorkDir();
|
|
5545
6232
|
const targetLang = config.lang || "en";
|
|
5546
|
-
const planPath =
|
|
5547
|
-
const legacyPlanPath =
|
|
6233
|
+
const planPath = join21(config.output.dir, `plan-${targetLang}.json`);
|
|
6234
|
+
const legacyPlanPath = join21(config.output.dir, "plan.json");
|
|
5548
6235
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
5549
|
-
await
|
|
6236
|
+
await mkdir9(config.output.dir, { recursive: true });
|
|
5550
6237
|
await setLogFilePath(config.output.dir);
|
|
5551
6238
|
try {
|
|
5552
6239
|
const plan = await runPlanPhase({
|
|
@@ -5557,7 +6244,8 @@ async function recordCommand(url, options = {}) {
|
|
|
5557
6244
|
...description !== void 0 && { description },
|
|
5558
6245
|
planPath,
|
|
5559
6246
|
legacyPlanPath,
|
|
5560
|
-
targetLang
|
|
6247
|
+
targetLang,
|
|
6248
|
+
...fresh !== void 0 && { fresh }
|
|
5561
6249
|
});
|
|
5562
6250
|
const validatedPlan = await runValidationPhase({
|
|
5563
6251
|
plan,
|
|
@@ -5582,49 +6270,58 @@ async function recordCommand(url, options = {}) {
|
|
|
5582
6270
|
await saveUserData({ localVideoCount: newCount });
|
|
5583
6271
|
const remaining = FREE_VIDEO_LIMIT - newCount;
|
|
5584
6272
|
if (remaining > 0) {
|
|
5585
|
-
console.log(
|
|
6273
|
+
console.log(chalk15.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
|
|
5586
6274
|
}
|
|
5587
6275
|
}
|
|
5588
6276
|
console.log();
|
|
5589
|
-
console.log(
|
|
6277
|
+
console.log(chalk15.bold.green(" \u2713 Done!"));
|
|
5590
6278
|
console.log();
|
|
5591
|
-
console.log(
|
|
6279
|
+
console.log(chalk15.cyan(` ${formatClickablePath(finalPath)}`));
|
|
5592
6280
|
console.log();
|
|
5593
6281
|
return finalPath;
|
|
5594
6282
|
} catch (err) {
|
|
5595
6283
|
await logError("Record Command", err);
|
|
5596
6284
|
if (rethrow) throw err;
|
|
5597
6285
|
console.log();
|
|
5598
|
-
console.log(
|
|
5599
|
-
console.log(
|
|
6286
|
+
console.log(chalk15.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
6287
|
+
console.log(chalk15.dim(` See ${logFilePath} for details`));
|
|
5600
6288
|
process.exit(1);
|
|
5601
6289
|
}
|
|
5602
6290
|
}
|
|
5603
6291
|
|
|
5604
6292
|
// src/commands/script-cmd.ts
|
|
5605
6293
|
import { readFile as readFile8 } from "fs/promises";
|
|
5606
|
-
import { join as
|
|
5607
|
-
import
|
|
6294
|
+
import { join as join22 } from "path";
|
|
6295
|
+
import chalk16 from "chalk";
|
|
5608
6296
|
|
|
5609
6297
|
// src/ai/narrate.ts
|
|
5610
6298
|
init_lib();
|
|
5611
|
-
|
|
6299
|
+
var SCRIPT_ACTIONS = /* @__PURE__ */ new Set(["click", "type", "scroll", "wait", "select", "check", "navigate"]);
|
|
6300
|
+
var NEEDS_TARGET = /* @__PURE__ */ new Set(["click", "type", "select", "check", "navigate"]);
|
|
6301
|
+
function isUsableStep(step) {
|
|
6302
|
+
if (!step?.action || !SCRIPT_ACTIONS.has(step.action)) return false;
|
|
6303
|
+
if (NEEDS_TARGET.has(step.action) && !step.target) return false;
|
|
6304
|
+
if (step.action === "navigate" && !/^https?:\/\//.test(step.target ?? "")) return false;
|
|
6305
|
+
return true;
|
|
6306
|
+
}
|
|
6307
|
+
async function generatePlanFromScript(captions, url, title, pageElementsText, lang, logError2, userEmail, tier = "free", task2) {
|
|
5612
6308
|
const systemPrompt = `You map narration sentences to web UI actions.
|
|
5613
|
-
Given page elements and a script, return
|
|
6309
|
+
Given page elements and a numbered script, return one action per script line.
|
|
5614
6310
|
|
|
5615
6311
|
RULES:
|
|
5616
|
-
- Return ONLY valid JSON
|
|
5617
|
-
-
|
|
6312
|
+
- Return ONLY valid JSON, no explanation, no captions
|
|
6313
|
+
- Return exactly one entry per script line, in order, with "line" set to that line's number
|
|
5618
6314
|
- Use page elements to pick realistic actions/targets
|
|
5619
|
-
- If a
|
|
6315
|
+
- If a line doesn't match a specific UI action, use { "action": "wait" }
|
|
6316
|
+
- Keep every target and value short
|
|
5620
6317
|
|
|
5621
6318
|
ACTION FORMATS:
|
|
5622
|
-
\u2022 click: { "action": "click", "target": "button text" }
|
|
5623
|
-
\u2022 type: { "action": "type", "target": "input selector", "value": "text
|
|
5624
|
-
\u2022 scroll: { "action": "scroll", "value": "up|down" }
|
|
5625
|
-
\u2022 wait: { "action": "wait" }
|
|
6319
|
+
\u2022 click: { "line": 1, "action": "click", "target": "button text" }
|
|
6320
|
+
\u2022 type: { "line": 2, "action": "type", "target": "input selector", "value": "text to type" }
|
|
6321
|
+
\u2022 scroll: { "line": 3, "action": "scroll", "value": "up|down" }
|
|
6322
|
+
\u2022 wait: { "line": 4, "action": "wait" }
|
|
5626
6323
|
|
|
5627
|
-
OUTPUT:
|
|
6324
|
+
OUTPUT: {"description":"one short sentence","steps":[ ... ]}`;
|
|
5628
6325
|
const scriptLines = captions.map((c, i) => `${i + 1}. "${c}"`).join("\n");
|
|
5629
6326
|
const userMessage = `URL: ${url}
|
|
5630
6327
|
App: ${title}
|
|
@@ -5632,48 +6329,108 @@ App: ${title}
|
|
|
5632
6329
|
Available page elements:
|
|
5633
6330
|
${pageElementsText}
|
|
5634
6331
|
|
|
5635
|
-
Narration script (map each line to a UI action
|
|
6332
|
+
Narration script (map each line to a UI action):
|
|
5636
6333
|
${scriptLines}
|
|
5637
6334
|
|
|
5638
|
-
Return
|
|
5639
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
5640
|
-
const
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
6335
|
+
Return JSON with a "description" and a "steps" array of exactly ${captions.length} actions.`;
|
|
6336
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "narration");
|
|
6337
|
+
const parsed = await parseAIJsonWithRepair(content, {
|
|
6338
|
+
logError: logError2,
|
|
6339
|
+
task: task2,
|
|
6340
|
+
userEmail,
|
|
6341
|
+
tier,
|
|
6342
|
+
shape: "The JSON should have: description (string), steps (array of {line, action, target, value}).",
|
|
6343
|
+
context: "Script Plan"
|
|
6344
|
+
});
|
|
6345
|
+
const raw = Array.isArray(parsed?.steps) ? parsed.steps : [];
|
|
6346
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
6347
|
+
raw.forEach((step, i) => {
|
|
6348
|
+
if (!step || typeof step !== "object") return;
|
|
6349
|
+
const line = typeof step.line === "number" && step.line >= 1 ? step.line : i + 1;
|
|
6350
|
+
if (!byLine.has(line)) byLine.set(line, step);
|
|
6351
|
+
});
|
|
6352
|
+
const steps = captions.map((caption, i) => {
|
|
6353
|
+
const mapped = byLine.get(i + 1);
|
|
6354
|
+
if (!isUsableStep(mapped)) {
|
|
6355
|
+
return { id: i + 1, action: "wait", caption };
|
|
6356
|
+
}
|
|
6357
|
+
return {
|
|
6358
|
+
id: i + 1,
|
|
6359
|
+
action: mapped.action,
|
|
6360
|
+
caption,
|
|
6361
|
+
...mapped.target ? { target: mapped.target } : {},
|
|
6362
|
+
...mapped.value ? { value: mapped.value } : {}
|
|
6363
|
+
};
|
|
6364
|
+
});
|
|
6365
|
+
return {
|
|
6366
|
+
url,
|
|
6367
|
+
title,
|
|
6368
|
+
description: typeof parsed?.description === "string" ? parsed.description : title,
|
|
6369
|
+
...lang !== void 0 ? { lang } : {},
|
|
6370
|
+
steps
|
|
6371
|
+
};
|
|
5651
6372
|
}
|
|
5652
6373
|
|
|
5653
6374
|
// src/commands/script-cmd.ts
|
|
5654
6375
|
init_lib();
|
|
5655
6376
|
init_config_loader();
|
|
6377
|
+
async function reusablePlan(planPath, lines) {
|
|
6378
|
+
try {
|
|
6379
|
+
const plan = JSON.parse(await readFile8(planPath, "utf-8"));
|
|
6380
|
+
if (!Array.isArray(plan.steps)) return null;
|
|
6381
|
+
if (plan.steps.length < lines.length) return null;
|
|
6382
|
+
const matches = lines.every((line, i) => plan.steps[i]?.caption === line);
|
|
6383
|
+
return matches ? plan : null;
|
|
6384
|
+
} catch {
|
|
6385
|
+
return null;
|
|
6386
|
+
}
|
|
6387
|
+
}
|
|
5656
6388
|
async function scriptCommand(url, scriptPath, lang) {
|
|
6389
|
+
url = resolveStartUrl(url);
|
|
5657
6390
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
6391
|
+
const userData = await loadUserData();
|
|
6392
|
+
const userTier = userData.email ? await getTierByEmail(userData.email) : "free";
|
|
5658
6393
|
header("Script");
|
|
5659
6394
|
const raw = await readFile8(scriptPath, "utf-8");
|
|
5660
6395
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
5661
6396
|
if (lines.length === 0) {
|
|
5662
|
-
console.log(
|
|
6397
|
+
console.log(chalk16.red(" Script file is empty or has no non-comment lines"));
|
|
5663
6398
|
process.exit(1);
|
|
5664
6399
|
}
|
|
5665
6400
|
info(`${lines.length} captions from script`);
|
|
6401
|
+
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
6402
|
+
if (lines.length > maxSteps) {
|
|
6403
|
+
console.log(
|
|
6404
|
+
chalk16.yellow(` \u26A0 ${lines.length} script lines exceeds plan.maxSteps (${maxSteps}) \u2014 recording all of them.`)
|
|
6405
|
+
);
|
|
6406
|
+
console.log(chalk16.dim(` Raise plan.maxSteps in quickpeek.config.json to silence this, or split the script.`));
|
|
6407
|
+
}
|
|
5666
6408
|
header("Analyze");
|
|
5667
6409
|
const crawlTask = task("Crawling page for UI elements");
|
|
5668
|
-
const { elements } = await crawlPage(url, () => crawlTask);
|
|
6410
|
+
const { elements } = await crawlPage(url, () => crawlTask, findAuthState(url));
|
|
5669
6411
|
const elementsText = elements.slice(0, 30).map((e) => ` ${e.action}: ${e.text} (${e.selector})`).join("\n");
|
|
5670
6412
|
header("Plan");
|
|
5671
6413
|
const planTask = task("Mapping script to UI actions");
|
|
5672
6414
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
5673
|
-
const
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
6415
|
+
const planPath = join22(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
6416
|
+
let plan = await reusablePlan(planPath, lines);
|
|
6417
|
+
if (plan) {
|
|
6418
|
+
planTask.succeed(`${plan.steps.length} steps reused from existing plan`);
|
|
6419
|
+
} else {
|
|
6420
|
+
plan = await generatePlanFromScript(
|
|
6421
|
+
lines,
|
|
6422
|
+
url,
|
|
6423
|
+
appTitle,
|
|
6424
|
+
elementsText,
|
|
6425
|
+
lang,
|
|
6426
|
+
logError,
|
|
6427
|
+
userData.email,
|
|
6428
|
+
userTier,
|
|
6429
|
+
() => planTask
|
|
6430
|
+
);
|
|
6431
|
+
await savePlan(plan, planPath);
|
|
6432
|
+
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
6433
|
+
}
|
|
5677
6434
|
header("Voiceover");
|
|
5678
6435
|
const { voiceoverPath, stepAudio } = await generateVoiceover(
|
|
5679
6436
|
plan.steps,
|
|
@@ -5689,14 +6446,116 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
5689
6446
|
const { videoPath } = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
5690
6447
|
header("Compose");
|
|
5691
6448
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5692
|
-
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config,
|
|
6449
|
+
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, userTier);
|
|
5693
6450
|
console.log();
|
|
5694
|
-
console.log(
|
|
5695
|
-
console.log(
|
|
6451
|
+
console.log(chalk16.bold.green(" Done!"));
|
|
6452
|
+
console.log(chalk16.cyan(` ${finalPath}`));
|
|
5696
6453
|
}
|
|
5697
6454
|
|
|
5698
6455
|
// src/mcp/tools.ts
|
|
5699
6456
|
init_SparkStripe_feature_gating();
|
|
6457
|
+
|
|
6458
|
+
// src/mcp/delegate.ts
|
|
6459
|
+
init_SparkStripe_feature_gating();
|
|
6460
|
+
var STEPS = {
|
|
6461
|
+
plan: {
|
|
6462
|
+
field: "plan",
|
|
6463
|
+
shape: "the demo plan, as the JSON object the instructions describe"
|
|
6464
|
+
},
|
|
6465
|
+
fix: {
|
|
6466
|
+
field: "fix",
|
|
6467
|
+
shape: "the corrected steps, as the JSON object the instructions describe"
|
|
6468
|
+
},
|
|
6469
|
+
narration: {
|
|
6470
|
+
field: "narration",
|
|
6471
|
+
shape: "the narration, as the JSON object the instructions describe"
|
|
6472
|
+
}
|
|
6473
|
+
};
|
|
6474
|
+
function textResult(text) {
|
|
6475
|
+
return { content: [{ type: "text", text }] };
|
|
6476
|
+
}
|
|
6477
|
+
function section(e, index) {
|
|
6478
|
+
const { field, shape } = STEPS[e.step];
|
|
6479
|
+
return [`### ${index}. ${field}`, ``, e.system, ``, e.prompt, ``, `Answer as \`${field}\`: ${shape}`].join("\n");
|
|
6480
|
+
}
|
|
6481
|
+
function delegatedBrief(pending2) {
|
|
6482
|
+
const tool = pending2[0]?.tool ?? "this tool";
|
|
6483
|
+
const fields = pending2.map((e) => STEPS[e.step].field);
|
|
6484
|
+
return textResult(
|
|
6485
|
+
[
|
|
6486
|
+
`\`${tool}\` needs ${pending2.length === 1 ? "a piece of" : `${pending2.length} pieces of`} writing,`,
|
|
6487
|
+
`and you are a stronger model than the one this CLI would relay to, so it is yours to`,
|
|
6488
|
+
`do. QuickPeek never generates on your behalf.`,
|
|
6489
|
+
``,
|
|
6490
|
+
...pending2.map((e, i) => `${section(e, i + 1)}
|
|
6491
|
+
`),
|
|
6492
|
+
`## Next`,
|
|
6493
|
+
``,
|
|
6494
|
+
`Call \`${tool}\` again with the same arguments plus \`ai\`:`,
|
|
6495
|
+
``,
|
|
6496
|
+
"```json",
|
|
6497
|
+
`{ "ai": { ${fields.map((f) => `"${f}": ...`).join(", ")} } }`,
|
|
6498
|
+
"```",
|
|
6499
|
+
``,
|
|
6500
|
+
`Each value is used verbatim and no further writing is asked for.`
|
|
6501
|
+
].join("\n")
|
|
6502
|
+
);
|
|
6503
|
+
}
|
|
6504
|
+
async function callerTier() {
|
|
6505
|
+
const { loadUserData: loadUserData2 } = await Promise.resolve().then(() => (init_config_loader(), config_loader_exports));
|
|
6506
|
+
const userData = await loadUserData2();
|
|
6507
|
+
if (!userData.email) return "free";
|
|
6508
|
+
try {
|
|
6509
|
+
return await getTierByEmail(userData.email);
|
|
6510
|
+
} catch {
|
|
6511
|
+
return "free";
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
async function withDelegation(tool, args, fn) {
|
|
6515
|
+
const answers = args.ai ?? {};
|
|
6516
|
+
const tier = await callerTier();
|
|
6517
|
+
try {
|
|
6518
|
+
const result = await runInMcpTool(tool, fn, answers);
|
|
6519
|
+
const pending2 = pendingDelegations();
|
|
6520
|
+
return pending2.length ? delegatedBrief(pending2) : withTierNote(result, tier);
|
|
6521
|
+
} catch (e) {
|
|
6522
|
+
const pending2 = pendingDelegations();
|
|
6523
|
+
if (pending2.length) return delegatedBrief(pending2);
|
|
6524
|
+
if (e instanceof DelegatedGenerationError) return delegatedBrief([e]);
|
|
6525
|
+
throw e;
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
function withTierNote(result, tier) {
|
|
6529
|
+
if (tier !== "free") return result;
|
|
6530
|
+
return {
|
|
6531
|
+
...result,
|
|
6532
|
+
content: [
|
|
6533
|
+
...result.content,
|
|
6534
|
+
{
|
|
6535
|
+
type: "text",
|
|
6536
|
+
text: `Free tier: this demo carries a watermark and the free video limit applies. Upgrade at ${pricingUrl()}`
|
|
6537
|
+
}
|
|
6538
|
+
]
|
|
6539
|
+
};
|
|
6540
|
+
}
|
|
6541
|
+
|
|
6542
|
+
// src/mcp/elicit.ts
|
|
6543
|
+
var elicit = null;
|
|
6544
|
+
var canPrompt = null;
|
|
6545
|
+
function setElicit(fn) {
|
|
6546
|
+
elicit = fn;
|
|
6547
|
+
}
|
|
6548
|
+
function setCanElicit(fn) {
|
|
6549
|
+
canPrompt = fn;
|
|
6550
|
+
}
|
|
6551
|
+
function canElicit() {
|
|
6552
|
+
return canPrompt ? canPrompt() : elicit !== null;
|
|
6553
|
+
}
|
|
6554
|
+
async function askUser(message) {
|
|
6555
|
+
return elicit ? elicit(message) : null;
|
|
6556
|
+
}
|
|
6557
|
+
|
|
6558
|
+
// src/mcp/tools.ts
|
|
5700
6559
|
var URL_PROPERTY = {
|
|
5701
6560
|
url: {
|
|
5702
6561
|
type: "string",
|
|
@@ -5709,15 +6568,27 @@ var LANG_PROPERTY = {
|
|
|
5709
6568
|
description: 'ISO 639-1 language for narration and captions (e.g. "en", "fr"). Default from config.'
|
|
5710
6569
|
}
|
|
5711
6570
|
};
|
|
6571
|
+
var SKIP_AUTH_PROPERTY = {
|
|
6572
|
+
skip_auth: {
|
|
6573
|
+
type: "boolean",
|
|
6574
|
+
description: "Record without signing in. By default, a URL with no saved session prompts the user to sign in first so the demo covers the app, not the logged-out landing page. Pass true only for public sites with nothing behind a login, or when a logged-out demo is intended."
|
|
6575
|
+
}
|
|
6576
|
+
};
|
|
5712
6577
|
var QUICKPEEK_TOOLS = [
|
|
5713
6578
|
{
|
|
5714
6579
|
name: "generate_demo",
|
|
5715
|
-
description: "Record a narrated demo video of a web app, end to end: an AI plans the walkthrough, Playwright drives a real browser, and the result is composed with voiceover and burned karaoke captions. This is the headline tool. It takes minutes, not seconds, because a real browser session is recorded. Returns the finished MP4 path and a file:// url.
|
|
6580
|
+
description: "Record a narrated demo video of a web app, end to end: an AI plans the walkthrough, Playwright drives a real browser, and the result is composed with voiceover and burned karaoke captions. This is the headline tool. It takes minutes, not seconds, because a real browser session is recorded. Returns the finished MP4 path and a file:// url. If the host has no saved session, the user is first prompted to sign in (a visible browser opens; QuickPeek never sees the password) so the demo starts inside the app, usually the dashboard, instead of the logged-out landing page. Pass skip_auth for public sites. The result says whether this recording was signed in.",
|
|
5716
6581
|
inputSchema: {
|
|
5717
6582
|
type: "object",
|
|
5718
6583
|
properties: {
|
|
6584
|
+
ai: {
|
|
6585
|
+
type: "object",
|
|
6586
|
+
description: `Writing you produced from this tool's brief, keyed by the fields it asked for (e.g. {"plan": {...}}). Each value is used verbatim and no AI call is made for it. Omit on the first call: the tool will tell you what it needs. This server never generates text itself - you are the model it delegates to.`,
|
|
6587
|
+
additionalProperties: true
|
|
6588
|
+
},
|
|
5719
6589
|
...URL_PROPERTY,
|
|
5720
6590
|
...LANG_PROPERTY,
|
|
6591
|
+
...SKIP_AUTH_PROPERTY,
|
|
5721
6592
|
description: {
|
|
5722
6593
|
type: "string",
|
|
5723
6594
|
description: "What the demo should focus on, in your words. Steers the AI plan. Without it, the planner infers the story from the page itself."
|
|
@@ -5736,20 +6607,49 @@ var QUICKPEEK_TOOLS = [
|
|
|
5736
6607
|
},
|
|
5737
6608
|
{
|
|
5738
6609
|
name: "generate_demo_from_script",
|
|
5739
|
-
description: "Record a demo that follows a script you supply, one caption per line, instead of an AI-planned walkthrough. The AI still maps each line to a UI action. Use when you know exactly what the video should say and in what order.",
|
|
6610
|
+
description: "Record a demo that follows a script you supply, one caption per line, instead of an AI-planned walkthrough. The AI still maps each line to a UI action. Use when you know exactly what the video should say and in what order. Prompts for sign-in like generate_demo when the host has no saved session.",
|
|
5740
6611
|
inputSchema: {
|
|
5741
6612
|
type: "object",
|
|
5742
6613
|
properties: {
|
|
6614
|
+
ai: {
|
|
6615
|
+
type: "object",
|
|
6616
|
+
description: `Writing you produced from this tool's brief, keyed by the fields it asked for (e.g. {"plan": {...}}). Each value is used verbatim and no AI call is made for it. Omit on the first call: the tool will tell you what it needs. This server never generates text itself - you are the model it delegates to.`,
|
|
6617
|
+
additionalProperties: true
|
|
6618
|
+
},
|
|
5743
6619
|
...URL_PROPERTY,
|
|
5744
6620
|
script_path: {
|
|
5745
6621
|
type: "string",
|
|
5746
6622
|
description: "Path to a plain text file with one sentence or caption per line."
|
|
5747
6623
|
},
|
|
5748
|
-
...LANG_PROPERTY
|
|
6624
|
+
...LANG_PROPERTY,
|
|
6625
|
+
...SKIP_AUTH_PROPERTY
|
|
5749
6626
|
},
|
|
5750
6627
|
required: ["url", "script_path"]
|
|
5751
6628
|
}
|
|
5752
6629
|
},
|
|
6630
|
+
{
|
|
6631
|
+
name: "open_in_editor",
|
|
6632
|
+
description: "Open a recorded demo's plan in the browser editor, where the user can reword captions, reorder steps or drop them. The plan travels in the URL fragment, so nothing is uploaded; saving in the editor writes back over the same file and the next recording picks it up. Records nothing itself.",
|
|
6633
|
+
inputSchema: {
|
|
6634
|
+
type: "object",
|
|
6635
|
+
properties: { ...URL_PROPERTY, ...LANG_PROPERTY },
|
|
6636
|
+
required: ["url"]
|
|
6637
|
+
}
|
|
6638
|
+
},
|
|
6639
|
+
{
|
|
6640
|
+
name: "start_auth",
|
|
6641
|
+
description: "Open a visible browser on a site so the user can sign in, once, by hand. Everything recorded afterwards starts from that session, and demos of the bare URL begin on the page the user leaves open when the session is saved \u2014 tell them to finish on their dashboard. QuickPeek never sees the password: only the resulting cookies are saved. On clients that support prompting, this saves the session as soon as the user confirms; otherwise call finish_auth when they say they are done.",
|
|
6642
|
+
inputSchema: {
|
|
6643
|
+
type: "object",
|
|
6644
|
+
properties: { ...URL_PROPERTY },
|
|
6645
|
+
required: ["url"]
|
|
6646
|
+
}
|
|
6647
|
+
},
|
|
6648
|
+
{
|
|
6649
|
+
name: "finish_auth",
|
|
6650
|
+
description: "Save the session from the browser window start_auth opened, and close it. Call this once the user says they have signed in.",
|
|
6651
|
+
inputSchema: { type: "object", properties: {} }
|
|
6652
|
+
},
|
|
5753
6653
|
{
|
|
5754
6654
|
name: "list_demos",
|
|
5755
6655
|
description: "List demo videos already produced for a URL, newest first, with size and modified time. Cheap: reads the output directory, records nothing.",
|
|
@@ -5779,9 +6679,38 @@ var QUICKPEEK_TOOLS = [
|
|
|
5779
6679
|
inputSchema: { type: "object", properties: {} }
|
|
5780
6680
|
}
|
|
5781
6681
|
];
|
|
6682
|
+
async function ensureSignedIn(url, skipAuth) {
|
|
6683
|
+
if (!url.startsWith("http") || skipAuth || findAuthState(url)) return null;
|
|
6684
|
+
const host = new URL(url).host;
|
|
6685
|
+
const authRequired = () => ({
|
|
6686
|
+
...jsonContent({
|
|
6687
|
+
status: "auth_required",
|
|
6688
|
+
host,
|
|
6689
|
+
message: `No saved sign-in for ${host}, so the demo would only show logged-out pages. Ask the user how to proceed: to demo the app signed in, call start_auth with this url, then finish_auth once they confirm, and retry. For a public site with no login, retry with skip_auth: true.`
|
|
6690
|
+
}),
|
|
6691
|
+
isError: true
|
|
6692
|
+
});
|
|
6693
|
+
if (!canElicit()) return authRequired();
|
|
6694
|
+
await openSignIn(url);
|
|
6695
|
+
const confirmed = await askUser(
|
|
6696
|
+
`No saved sign-in for ${host}. A browser window is open: sign in there and leave it on the page the demo should start from (usually your dashboard), then confirm. Decline to record logged out.`
|
|
6697
|
+
);
|
|
6698
|
+
if (confirmed === null) {
|
|
6699
|
+
await cancelSignIn();
|
|
6700
|
+
return authRequired();
|
|
6701
|
+
}
|
|
6702
|
+
if (confirmed) {
|
|
6703
|
+
await saveSignIn();
|
|
6704
|
+
return null;
|
|
6705
|
+
}
|
|
6706
|
+
await cancelSignIn();
|
|
6707
|
+
return null;
|
|
6708
|
+
}
|
|
5782
6709
|
async function handleGenerateDemo(args) {
|
|
5783
6710
|
const url = resolveUrl(args.url);
|
|
5784
6711
|
return withSilencedStdout(async () => {
|
|
6712
|
+
const authGate = await ensureSignedIn(url, args.skip_auth);
|
|
6713
|
+
if (authGate) return authGate;
|
|
5785
6714
|
const output = await recordCommand(url, {
|
|
5786
6715
|
langOverride: args.lang,
|
|
5787
6716
|
description: args.description,
|
|
@@ -5793,7 +6722,11 @@ async function handleGenerateDemo(args) {
|
|
|
5793
6722
|
rethrow: true
|
|
5794
6723
|
});
|
|
5795
6724
|
if (!output) return errorContent("Recording finished without producing a video.");
|
|
5796
|
-
return fileResult(output, {
|
|
6725
|
+
return fileResult(output, {
|
|
6726
|
+
name: output.split("/").pop(),
|
|
6727
|
+
url_recorded: resolveStartUrl(url),
|
|
6728
|
+
auth: authStatus(url)
|
|
6729
|
+
});
|
|
5797
6730
|
});
|
|
5798
6731
|
}
|
|
5799
6732
|
function authStatus(url) {
|
|
@@ -5803,23 +6736,74 @@ function authStatus(url) {
|
|
|
5803
6736
|
return {
|
|
5804
6737
|
signedIn: false,
|
|
5805
6738
|
host,
|
|
5806
|
-
note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login,
|
|
6739
|
+
note: `No saved session for ${host}, so only pages reachable while logged out were recorded. To cover pages behind the login, call start_auth with this url (or have the user run \`qp auth ${url}\`), then re-run with fresh: true and without skip_auth.`
|
|
5807
6740
|
};
|
|
5808
6741
|
}
|
|
5809
6742
|
async function handleGenerateDemoFromScript(args) {
|
|
5810
6743
|
const url = resolveUrl(args.url);
|
|
5811
6744
|
const scriptPath = resolveInputPath(args.script_path);
|
|
5812
6745
|
return withSilencedStdout(async () => {
|
|
6746
|
+
const authGate = await ensureSignedIn(url, args.skip_auth);
|
|
6747
|
+
if (authGate) return authGate;
|
|
5813
6748
|
await scriptCommand(url, scriptPath, args.lang);
|
|
5814
6749
|
const dir = getOutputDirFromUrl(url);
|
|
5815
6750
|
const newest = newestVideo(dir);
|
|
5816
6751
|
if (!newest) return errorContent(`No video found in ${dir} after running the script.`);
|
|
5817
|
-
return fileResult(newest, { name: newest.split("/").pop(), url_recorded: url, scriptPath });
|
|
6752
|
+
return fileResult(newest, { name: newest.split("/").pop(), url_recorded: resolveStartUrl(url), scriptPath });
|
|
6753
|
+
});
|
|
6754
|
+
}
|
|
6755
|
+
async function handleOpenInEditor(args) {
|
|
6756
|
+
const url = resolveUrl(args.url);
|
|
6757
|
+
return withSilencedStdout(async () => {
|
|
6758
|
+
const dir = getOutputDirFromUrl(url);
|
|
6759
|
+
const planPath = findPlanFile(dir, args.lang);
|
|
6760
|
+
if (!planPath) {
|
|
6761
|
+
return errorContent(`No plan found in ${dir}. Record the demo first with generate_demo.`);
|
|
6762
|
+
}
|
|
6763
|
+
const parsed = parsePlanText(readFileSync7(planPath, "utf8"));
|
|
6764
|
+
if (!parsed.ok) return errorContent(`${planPath} cannot be opened: ${parsed.error}`);
|
|
6765
|
+
const base = editorBaseUrl();
|
|
6766
|
+
const editorUrl = planEditorUrl(base, parsed.plan);
|
|
6767
|
+
await openInBrowser(editorUrl ?? `${base}/edit`);
|
|
6768
|
+
return jsonContent({
|
|
6769
|
+
opened: true,
|
|
6770
|
+
plan_path: planPath,
|
|
6771
|
+
steps: parsed.plan.steps.length,
|
|
6772
|
+
url: editorUrl ?? `${base}/edit`,
|
|
6773
|
+
next_steps: editorUrl ? ["The editor opens with the plan loaded.", "Saving there writes back over plan_path; then re-record."] : [`This plan is too long for a link. Load ${planPath} in the editor by hand.`]
|
|
6774
|
+
});
|
|
6775
|
+
});
|
|
6776
|
+
}
|
|
6777
|
+
async function handleStartAuth(args) {
|
|
6778
|
+
const url = resolveUrl(args.url);
|
|
6779
|
+
return withSilencedStdout(async () => {
|
|
6780
|
+
await openSignIn(url);
|
|
6781
|
+
const confirmed = await askUser(
|
|
6782
|
+
`A browser window is open on ${url}. Sign in there, leave it on the page demos should start from (usually your dashboard), then confirm.`
|
|
6783
|
+
);
|
|
6784
|
+
if (confirmed === null) {
|
|
6785
|
+
return jsonContent({
|
|
6786
|
+
status: "awaiting_sign_in",
|
|
6787
|
+
url,
|
|
6788
|
+
next: "A browser window is open. Call finish_auth once the user says they are signed in."
|
|
6789
|
+
});
|
|
6790
|
+
}
|
|
6791
|
+
if (!confirmed) {
|
|
6792
|
+
await cancelSignIn();
|
|
6793
|
+
return jsonContent({ status: "cancelled", url, note: "Nothing was saved." });
|
|
6794
|
+
}
|
|
6795
|
+
return jsonContent({ status: "saved", ...await saveSignIn() });
|
|
6796
|
+
});
|
|
6797
|
+
}
|
|
6798
|
+
async function handleFinishAuth() {
|
|
6799
|
+
return withSilencedStdout(async () => {
|
|
6800
|
+
if (!pendingSignIn()) return errorContent("No sign-in window is open. Call start_auth first.");
|
|
6801
|
+
return jsonContent({ status: "saved", ...await saveSignIn() });
|
|
5818
6802
|
});
|
|
5819
6803
|
}
|
|
5820
6804
|
function newestVideo(dir) {
|
|
5821
6805
|
try {
|
|
5822
|
-
const videos =
|
|
6806
|
+
const videos = readdirSync5(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => join23(dir, f)).map((p) => ({ path: p, mtime: statSync2(p).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
5823
6807
|
return videos[0]?.path ?? null;
|
|
5824
6808
|
} catch {
|
|
5825
6809
|
return null;
|
|
@@ -5831,8 +6815,8 @@ async function handleListDemos(args) {
|
|
|
5831
6815
|
return withSilencedStdout(async () => {
|
|
5832
6816
|
let entries = [];
|
|
5833
6817
|
try {
|
|
5834
|
-
entries =
|
|
5835
|
-
const s = statSync2(
|
|
6818
|
+
entries = readdirSync5(dir).filter((f) => f.toLowerCase().endsWith(".mp4")).map((f) => {
|
|
6819
|
+
const s = statSync2(join23(dir, f));
|
|
5836
6820
|
return { name: f, bytes: s.size, modified: new Date(s.mtimeMs).toISOString() };
|
|
5837
6821
|
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
5838
6822
|
} catch {
|
|
@@ -5876,8 +6860,11 @@ async function handleCheckAccount() {
|
|
|
5876
6860
|
});
|
|
5877
6861
|
}
|
|
5878
6862
|
var QUICKPEEK_HANDLERS = {
|
|
5879
|
-
generate_demo: handleGenerateDemo,
|
|
5880
|
-
generate_demo_from_script: handleGenerateDemoFromScript,
|
|
6863
|
+
generate_demo: (args) => withDelegation("generate_demo", args, () => handleGenerateDemo(args)),
|
|
6864
|
+
generate_demo_from_script: (args) => withDelegation("generate_demo_from_script", args, () => handleGenerateDemoFromScript(args)),
|
|
6865
|
+
open_in_editor: handleOpenInEditor,
|
|
6866
|
+
start_auth: handleStartAuth,
|
|
6867
|
+
finish_auth: handleFinishAuth,
|
|
5881
6868
|
list_demos: handleListDemos,
|
|
5882
6869
|
get_demo_config: handleGetDemoConfig,
|
|
5883
6870
|
list_voices: handleListVoices,
|
|
@@ -5905,5 +6892,7 @@ export {
|
|
|
5905
6892
|
TOOLS,
|
|
5906
6893
|
TOOL_HANDLERS,
|
|
5907
6894
|
errorContent,
|
|
5908
|
-
jsonContent
|
|
6895
|
+
jsonContent,
|
|
6896
|
+
setCanElicit,
|
|
6897
|
+
setElicit
|
|
5909
6898
|
};
|