@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/qp.js
CHANGED
|
@@ -59,7 +59,7 @@ function getErrorMessage(err) {
|
|
|
59
59
|
return err instanceof Error ? err.message : String(err);
|
|
60
60
|
}
|
|
61
61
|
function toTitleCase(text) {
|
|
62
|
-
return text.replace(/\w
|
|
62
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
63
63
|
if (index === 0) {
|
|
64
64
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
65
65
|
}
|
|
@@ -125,6 +125,23 @@ var init_utils = __esm({
|
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
// src/lib/api-client.ts
|
|
128
|
+
function apiKey() {
|
|
129
|
+
return process.env.SPARK_AI_KEY || "";
|
|
130
|
+
}
|
|
131
|
+
function hostOf2(url) {
|
|
132
|
+
try {
|
|
133
|
+
return new URL(url).host;
|
|
134
|
+
} catch {
|
|
135
|
+
return "AI service";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function firstLine(text) {
|
|
139
|
+
return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
|
|
140
|
+
}
|
|
141
|
+
function truncate(text, max) {
|
|
142
|
+
const trimmed = (text ?? "").trim();
|
|
143
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
|
|
144
|
+
}
|
|
128
145
|
async function getOrRefreshDirectToken() {
|
|
129
146
|
const now = Date.now();
|
|
130
147
|
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
@@ -133,7 +150,7 @@ async function getOrRefreshDirectToken() {
|
|
|
133
150
|
try {
|
|
134
151
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
135
152
|
method: "POST",
|
|
136
|
-
headers: { "X-Api-Key":
|
|
153
|
+
headers: { "X-Api-Key": apiKey() }
|
|
137
154
|
});
|
|
138
155
|
if (!response.ok) {
|
|
139
156
|
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
@@ -176,8 +193,10 @@ async function getOrRefreshRelayToken(email) {
|
|
|
176
193
|
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
177
194
|
}
|
|
178
195
|
}
|
|
179
|
-
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
180
|
-
|
|
196
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
|
|
197
|
+
const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
|
|
198
|
+
let lastRetriableError = null;
|
|
199
|
+
const host = hostOf2(chatUrl);
|
|
181
200
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
182
201
|
try {
|
|
183
202
|
const response = await fetch(chatUrl, {
|
|
@@ -193,36 +212,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
193
212
|
]
|
|
194
213
|
})
|
|
195
214
|
});
|
|
215
|
+
const bodyText = await response.text().catch(() => "");
|
|
196
216
|
let data = {};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
try {
|
|
218
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
219
|
+
} catch {
|
|
200
220
|
}
|
|
201
221
|
if (!response.ok) {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
|
|
223
|
+
const error = `${host} ${response.status}: ${upstream}`;
|
|
224
|
+
const detail = truncate(bodyText, 800);
|
|
225
|
+
const isRetriable = response.status === 429 || response.status >= 500;
|
|
226
|
+
if (isRetriable && attempt < 3) {
|
|
227
|
+
const retryAfter2 = response.headers.get("Retry-After");
|
|
228
|
+
const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
|
|
229
|
+
onRetry?.({ attempt, status: response.status, delayMs, host });
|
|
230
|
+
lastRetriableError = {
|
|
207
231
|
success: false,
|
|
208
232
|
error,
|
|
209
|
-
isRateLimit:
|
|
210
|
-
|
|
233
|
+
isRateLimit: response.status === 429,
|
|
234
|
+
status: response.status,
|
|
235
|
+
host,
|
|
236
|
+
...detail ? { detail } : {},
|
|
237
|
+
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
211
238
|
};
|
|
212
239
|
await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
|
|
213
240
|
continue;
|
|
214
241
|
}
|
|
242
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
243
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
215
244
|
return {
|
|
216
245
|
success: false,
|
|
217
246
|
error,
|
|
218
247
|
isRateLimit: response.status === 429,
|
|
219
|
-
isAuthError: response.status === 401 || response.status === 403
|
|
248
|
+
isAuthError: response.status === 401 || response.status === 403,
|
|
249
|
+
status: response.status,
|
|
250
|
+
host,
|
|
251
|
+
...detail ? { detail } : {},
|
|
252
|
+
...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
|
|
220
253
|
};
|
|
221
254
|
}
|
|
222
255
|
if (!data.success || !data.response) {
|
|
223
256
|
return {
|
|
224
257
|
success: false,
|
|
225
|
-
error: data.error || "
|
|
258
|
+
error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
|
|
259
|
+
status: response.status,
|
|
260
|
+
host,
|
|
261
|
+
...bodyText ? { detail: truncate(bodyText, 800) } : {}
|
|
226
262
|
};
|
|
227
263
|
}
|
|
228
264
|
return {
|
|
@@ -236,15 +272,17 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
236
272
|
}
|
|
237
273
|
};
|
|
238
274
|
} catch (err) {
|
|
239
|
-
if (attempt === 3)
|
|
275
|
+
if (attempt === 3) {
|
|
276
|
+
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
277
|
+
}
|
|
240
278
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
241
279
|
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
242
280
|
}
|
|
243
281
|
}
|
|
244
|
-
return
|
|
282
|
+
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
245
283
|
}
|
|
246
|
-
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
247
|
-
if (!
|
|
284
|
+
async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
285
|
+
if (!apiKey()) {
|
|
248
286
|
return {
|
|
249
287
|
success: false,
|
|
250
288
|
error: "AI service is not configured. Please report this issue.",
|
|
@@ -253,12 +291,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
|
253
291
|
}
|
|
254
292
|
try {
|
|
255
293
|
const token = await getOrRefreshDirectToken();
|
|
256
|
-
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
294
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
|
|
257
295
|
} catch (err) {
|
|
258
296
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
259
297
|
}
|
|
260
298
|
}
|
|
261
|
-
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
299
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
|
|
262
300
|
if (!email) {
|
|
263
301
|
return {
|
|
264
302
|
success: false,
|
|
@@ -268,11 +306,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
|
268
306
|
}
|
|
269
307
|
try {
|
|
270
308
|
let token = await getOrRefreshRelayToken(email);
|
|
271
|
-
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
309
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
272
310
|
if (!result.success && result.isAuthError) {
|
|
273
311
|
cachedRelayToken = null;
|
|
274
312
|
token = await getOrRefreshRelayToken(email);
|
|
275
|
-
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
313
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
276
314
|
}
|
|
277
315
|
return result;
|
|
278
316
|
} catch (err) {
|
|
@@ -304,18 +342,19 @@ function normalizeUrl(input) {
|
|
|
304
342
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
305
343
|
return `https://${url}`;
|
|
306
344
|
}
|
|
307
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL,
|
|
345
|
+
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;
|
|
308
346
|
var init_api_client = __esm({
|
|
309
347
|
"src/lib/api-client.ts"() {
|
|
310
348
|
"use strict";
|
|
311
349
|
init_utils();
|
|
350
|
+
SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
351
|
+
CLI_BACKOFF_MS = [1e4, 3e4];
|
|
312
352
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
313
353
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
314
354
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
315
355
|
RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
316
356
|
RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
317
357
|
RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
318
|
-
API_KEY = process.env.SPARK_AI_KEY || "";
|
|
319
358
|
cachedDirectToken = null;
|
|
320
359
|
cachedRelayToken = null;
|
|
321
360
|
}
|
|
@@ -340,7 +379,7 @@ async function getStatusCached(email) {
|
|
|
340
379
|
function getTier(d) {
|
|
341
380
|
if (!d.subscription || !d.registered) return "free";
|
|
342
381
|
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
343
|
-
return TIERS[d.subscription.price_id] ?? "
|
|
382
|
+
return TIERS[d.subscription.price_id] ?? "pro";
|
|
344
383
|
}
|
|
345
384
|
async function getTierByEmail(email) {
|
|
346
385
|
try {
|
|
@@ -357,17 +396,18 @@ async function isVerified(email) {
|
|
|
357
396
|
}
|
|
358
397
|
}
|
|
359
398
|
function pricingUrl(opts) {
|
|
360
|
-
const u = new URL(`${BASE}
|
|
399
|
+
const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
|
|
361
400
|
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
362
401
|
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
363
402
|
return u.toString();
|
|
364
403
|
}
|
|
365
|
-
var APP_ID, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
404
|
+
var APP_ID, OWNER_SLUG, BASE, TIERS, RANK, FEATURES, getStatus, _cache, isPaid, hasAccess, hasTrialRemaining, showPaywall, isCanceling;
|
|
366
405
|
var init_SparkStripe_feature_gating = __esm({
|
|
367
406
|
"src/lib/SparkStripe-feature-gating.ts"() {
|
|
368
407
|
"use strict";
|
|
369
408
|
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
370
|
-
|
|
409
|
+
OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
|
|
410
|
+
BASE = "https://sparkpay.dev";
|
|
371
411
|
TIERS = {
|
|
372
412
|
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
373
413
|
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
@@ -419,7 +459,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
|
|
|
419
459
|
var init_config = __esm({
|
|
420
460
|
"src/lib/config.ts"() {
|
|
421
461
|
"use strict";
|
|
422
|
-
VERSION = "1.
|
|
462
|
+
VERSION = "1.2.1";
|
|
423
463
|
CONFIG_FILE = "quickpeek.config.json";
|
|
424
464
|
DEFAULT_CONFIG = {
|
|
425
465
|
plan: { maxSteps: 8 },
|
|
@@ -569,6 +609,9 @@ var init_scraping = __esm({
|
|
|
569
609
|
});
|
|
570
610
|
|
|
571
611
|
// src/lib/crawl.ts
|
|
612
|
+
function idSelector(id) {
|
|
613
|
+
return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
|
|
614
|
+
}
|
|
572
615
|
function getActionForType(type) {
|
|
573
616
|
switch (type) {
|
|
574
617
|
case "button":
|
|
@@ -586,20 +629,29 @@ function getActionForType(type) {
|
|
|
586
629
|
return "type";
|
|
587
630
|
}
|
|
588
631
|
}
|
|
589
|
-
async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
590
|
-
const
|
|
632
|
+
async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
633
|
+
const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
|
|
634
|
+
const cached = crawlCache.get(cacheKey);
|
|
591
635
|
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
592
636
|
task2("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
593
|
-
return { title: cached.title, elements: [...cached.elements] };
|
|
637
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
594
638
|
}
|
|
595
639
|
const browserTask = task2("Launching browser");
|
|
596
|
-
const browser = await
|
|
597
|
-
const context = await browser.newContext(
|
|
640
|
+
const browser = await import_playwright3.chromium.launch({ headless: true });
|
|
641
|
+
const context = await browser.newContext({
|
|
642
|
+
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
643
|
+
// Crawl at the size we will record at, or the measurements below describe
|
|
644
|
+
// a page nobody will ever see.
|
|
645
|
+
...viewport ? { viewport } : {}
|
|
646
|
+
});
|
|
598
647
|
const page = await context.newPage();
|
|
599
648
|
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
600
649
|
const crawlTask = task2("Crawling page");
|
|
601
650
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
602
651
|
const title = await page.title();
|
|
652
|
+
const scrollable = await page.evaluate(
|
|
653
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
654
|
+
);
|
|
603
655
|
const rawElements = await page.evaluate(`
|
|
604
656
|
(() => {
|
|
605
657
|
const items = [];
|
|
@@ -689,9 +741,9 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
689
741
|
} else if (el.isFile || el.type === "file") {
|
|
690
742
|
selector = 'input[type="file"]';
|
|
691
743
|
} else if (el.type === "range") {
|
|
692
|
-
selector = el.id ?
|
|
744
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
693
745
|
} else if (el.type === "checkbox") {
|
|
694
|
-
selector = el.id ?
|
|
746
|
+
selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
|
|
695
747
|
} else if (el.type === "button") {
|
|
696
748
|
if (btnTexts[el.text] === 1) {
|
|
697
749
|
selector = `text="${el.text}"`;
|
|
@@ -701,7 +753,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
701
753
|
} else if (el.type === "link") {
|
|
702
754
|
selector = `text="${el.text}"`;
|
|
703
755
|
} else if (el.id) {
|
|
704
|
-
selector =
|
|
756
|
+
selector = idSelector(el.id);
|
|
705
757
|
} else if (el.name) {
|
|
706
758
|
selector = `[name="${el.name}"]`;
|
|
707
759
|
} else if (el.placeholder) {
|
|
@@ -729,18 +781,18 @@ async function crawlPage(url, task2 = noopTask, storageStatePath) {
|
|
|
729
781
|
});
|
|
730
782
|
await browser.close();
|
|
731
783
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
732
|
-
crawlCache.set(
|
|
784
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
733
785
|
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
734
786
|
const oldest = crawlCache.keys().next().value;
|
|
735
787
|
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
736
788
|
}
|
|
737
|
-
return { title, elements };
|
|
789
|
+
return { title, elements, scrollable };
|
|
738
790
|
}
|
|
739
|
-
var
|
|
791
|
+
var import_playwright3, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
740
792
|
var init_crawl = __esm({
|
|
741
793
|
"src/lib/crawl.ts"() {
|
|
742
794
|
"use strict";
|
|
743
|
-
|
|
795
|
+
import_playwright3 = require("playwright");
|
|
744
796
|
init_scraping();
|
|
745
797
|
noopTask = () => ({ succeed: () => {
|
|
746
798
|
} });
|
|
@@ -802,9 +854,9 @@ function convertArgToWSL(arg2) {
|
|
|
802
854
|
function spawnFFmpeg(cmd2, args) {
|
|
803
855
|
if (isWindowsRuntime) {
|
|
804
856
|
const wslArgs = args.map(convertArgToWSL);
|
|
805
|
-
return (0,
|
|
857
|
+
return (0, import_node_child_process2.spawn)("wsl", ["-e", cmd2, ...wslArgs]);
|
|
806
858
|
}
|
|
807
|
-
return (0,
|
|
859
|
+
return (0, import_node_child_process2.spawn)(cmd2, args);
|
|
808
860
|
}
|
|
809
861
|
function runFFmpeg(args, errorLabel) {
|
|
810
862
|
return new Promise((resolve9, reject) => {
|
|
@@ -820,11 +872,11 @@ function runFFmpeg(args, errorLabel) {
|
|
|
820
872
|
proc.on("error", reject);
|
|
821
873
|
});
|
|
822
874
|
}
|
|
823
|
-
var
|
|
875
|
+
var import_node_child_process2, isWindowsRuntime;
|
|
824
876
|
var init_spawn = __esm({
|
|
825
877
|
"src/lib/ffmpeg/spawn.ts"() {
|
|
826
878
|
"use strict";
|
|
827
|
-
|
|
879
|
+
import_node_child_process2 = require("child_process");
|
|
828
880
|
init_utils();
|
|
829
881
|
isWindowsRuntime = process.platform === "win32";
|
|
830
882
|
}
|
|
@@ -1005,8 +1057,39 @@ function getMediaDuration(filePath) {
|
|
|
1005
1057
|
proc.on("error", reject);
|
|
1006
1058
|
});
|
|
1007
1059
|
}
|
|
1060
|
+
function getAudioCodec(filePath) {
|
|
1061
|
+
return new Promise((resolve9) => {
|
|
1062
|
+
const args = [
|
|
1063
|
+
"-v",
|
|
1064
|
+
"error",
|
|
1065
|
+
"-select_streams",
|
|
1066
|
+
"a:0",
|
|
1067
|
+
"-show_entries",
|
|
1068
|
+
"stream=codec_name",
|
|
1069
|
+
"-of",
|
|
1070
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
1071
|
+
filePath
|
|
1072
|
+
];
|
|
1073
|
+
const proc = spawnFFmpeg("ffprobe", args);
|
|
1074
|
+
let stdout = "";
|
|
1075
|
+
proc.stdout.on("data", (data) => {
|
|
1076
|
+
stdout += data;
|
|
1077
|
+
});
|
|
1078
|
+
proc.on("close", (code) => resolve9(code === 0 ? stdout.trim() : ""));
|
|
1079
|
+
proc.on("error", () => resolve9(""));
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
async function isHonestWav(filePath) {
|
|
1083
|
+
const codec = await getAudioCodec(filePath);
|
|
1084
|
+
return codec.startsWith("pcm_");
|
|
1085
|
+
}
|
|
1086
|
+
function buildConcatArgs(listPath, output) {
|
|
1087
|
+
const isWav = output.toLowerCase().endsWith(".wav");
|
|
1088
|
+
const codec = isWav ? ["-c:a", "pcm_s16le"] : ["-c", "copy"];
|
|
1089
|
+
return ["-y", "-f", "concat", "-safe", "0", "-i", listPath, ...codec, output];
|
|
1090
|
+
}
|
|
1008
1091
|
function concatMedia(listPath, output) {
|
|
1009
|
-
return runFFmpeg(
|
|
1092
|
+
return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
|
|
1010
1093
|
}
|
|
1011
1094
|
function generateSilence(outputPath, durationMs) {
|
|
1012
1095
|
return runFFmpeg(
|
|
@@ -1255,6 +1338,114 @@ var init_ffmpeg2 = __esm({
|
|
|
1255
1338
|
}
|
|
1256
1339
|
});
|
|
1257
1340
|
|
|
1341
|
+
// src/lib/json-repair.ts
|
|
1342
|
+
function repairUnicodeEscapes(content) {
|
|
1343
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
1344
|
+
const code = Number.parseInt(hex, 16);
|
|
1345
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
1346
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
1347
|
+
}
|
|
1348
|
+
function salvageTruncatedJson(content) {
|
|
1349
|
+
const stack = [];
|
|
1350
|
+
const cuts = [];
|
|
1351
|
+
let inString = false;
|
|
1352
|
+
let escaped = false;
|
|
1353
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
1354
|
+
for (let i = 0; i < content.length; i++) {
|
|
1355
|
+
const ch = content[i];
|
|
1356
|
+
if (inString) {
|
|
1357
|
+
if (escaped) escaped = false;
|
|
1358
|
+
else if (ch === "\\") escaped = true;
|
|
1359
|
+
else if (ch === '"') {
|
|
1360
|
+
inString = false;
|
|
1361
|
+
record(i + 1, false);
|
|
1362
|
+
}
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
if (ch === '"') {
|
|
1366
|
+
inString = true;
|
|
1367
|
+
} else if (ch === "{" || ch === "[") {
|
|
1368
|
+
stack.push(ch);
|
|
1369
|
+
} else if (ch === "}" || ch === "]") {
|
|
1370
|
+
stack.pop();
|
|
1371
|
+
record(i + 1, true);
|
|
1372
|
+
} else if (ch === ",") {
|
|
1373
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
const tryCut = (cut) => {
|
|
1377
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
1378
|
+
if (head.endsWith(":")) return null;
|
|
1379
|
+
let closed = head;
|
|
1380
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
1381
|
+
closed += CLOSERS[cut.open[d]];
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
JSON.parse(closed);
|
|
1385
|
+
return closed;
|
|
1386
|
+
} catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
1391
|
+
for (const containerOnly of [true, false]) {
|
|
1392
|
+
for (let n = 0; n < attempts; n++) {
|
|
1393
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
1394
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
1395
|
+
const salvaged = tryCut(cut);
|
|
1396
|
+
if (salvaged !== null) return salvaged;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
function extractJsonCandidates(content) {
|
|
1402
|
+
let text = content.trim();
|
|
1403
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
1404
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
1405
|
+
const start = text.search(/[{[]/);
|
|
1406
|
+
if (start === -1) return [];
|
|
1407
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
1408
|
+
const end = text.lastIndexOf(closer);
|
|
1409
|
+
const candidates = [text.slice(start)];
|
|
1410
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
1411
|
+
return candidates;
|
|
1412
|
+
}
|
|
1413
|
+
function parseAIJson(content) {
|
|
1414
|
+
const candidates = extractJsonCandidates(content);
|
|
1415
|
+
if (candidates.length === 0) return null;
|
|
1416
|
+
for (const text of candidates) {
|
|
1417
|
+
try {
|
|
1418
|
+
return { value: JSON.parse(text), repaired: false };
|
|
1419
|
+
} catch {
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (const text of candidates) {
|
|
1423
|
+
try {
|
|
1424
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
for (const text of candidates) {
|
|
1429
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
1430
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
1431
|
+
if (salvaged === null) continue;
|
|
1432
|
+
try {
|
|
1433
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
1434
|
+
} catch {
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
var MAX_SALVAGE_ATTEMPTS, CLOSERS;
|
|
1441
|
+
var init_json_repair = __esm({
|
|
1442
|
+
"src/lib/json-repair.ts"() {
|
|
1443
|
+
"use strict";
|
|
1444
|
+
MAX_SALVAGE_ATTEMPTS = 500;
|
|
1445
|
+
CLOSERS = { "{": "}", "[": "]" };
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1258
1449
|
// src/lib/prompts.ts
|
|
1259
1450
|
function buildSystemPrompt(maxSteps) {
|
|
1260
1451
|
let scope;
|
|
@@ -1308,7 +1499,7 @@ EXAMPLES:
|
|
|
1308
1499
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
1309
1500
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
1310
1501
|
}
|
|
1311
|
-
function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
|
|
1502
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
1312
1503
|
const lang = langName ? `
|
|
1313
1504
|
Language: Write ALL captions in ${langName}.` : "";
|
|
1314
1505
|
const purpose = description ? `
|
|
@@ -1346,8 +1537,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
1346
1537
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
1347
1538
|
}
|
|
1348
1539
|
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
1540
|
+
const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
|
|
1541
|
+
const scrolling = cannotScroll ? `
|
|
1542
|
+
|
|
1543
|
+
SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
|
|
1544
|
+
const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
|
|
1349
1545
|
return `URL: ${url}
|
|
1350
|
-
Title: ${title}${purpose}
|
|
1546
|
+
Title: ${title}${purpose}${scrolling}
|
|
1351
1547
|
|
|
1352
1548
|
AVAILABLE ELEMENTS (grouped by action):
|
|
1353
1549
|
${elementSummary}
|
|
@@ -1357,7 +1553,7 @@ REQUIREMENTS:
|
|
|
1357
1553
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
1358
1554
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
1359
1555
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
1360
|
-
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
1556
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
|
|
1361
1557
|
|
|
1362
1558
|
Return JSON:
|
|
1363
1559
|
{
|
|
@@ -1380,10 +1576,12 @@ TARGET SYNTAX:
|
|
|
1380
1576
|
\u2022 Sliders: input[type="range"]
|
|
1381
1577
|
\u2022 Text inputs: Use selector from list above`;
|
|
1382
1578
|
}
|
|
1579
|
+
var MIN_USEFUL_SCROLL_PX;
|
|
1383
1580
|
var init_prompts = __esm({
|
|
1384
1581
|
"src/lib/prompts.ts"() {
|
|
1385
1582
|
"use strict";
|
|
1386
1583
|
init_utils();
|
|
1584
|
+
MIN_USEFUL_SCROLL_PX = 200;
|
|
1387
1585
|
}
|
|
1388
1586
|
});
|
|
1389
1587
|
|
|
@@ -1449,7 +1647,7 @@ var init_styles = __esm({
|
|
|
1449
1647
|
async function registerFreeUser(email) {
|
|
1450
1648
|
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1451
1649
|
try {
|
|
1452
|
-
const response = await fetch(`https://
|
|
1650
|
+
const response = await fetch(`https://sparkpay.dev/api/register-free`, {
|
|
1453
1651
|
method: "POST",
|
|
1454
1652
|
headers: { "Content-Type": "application/json" },
|
|
1455
1653
|
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
@@ -1516,7 +1714,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1516
1714
|
return 0;
|
|
1517
1715
|
}
|
|
1518
1716
|
const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
|
|
1519
|
-
if (!(0,
|
|
1717
|
+
if (!(0, import_node_fs3.existsSync)(mediaPath)) {
|
|
1520
1718
|
return 0;
|
|
1521
1719
|
}
|
|
1522
1720
|
try {
|
|
@@ -1538,8 +1736,8 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1538
1736
|
async function loadVoiceoverMeta(outputDir) {
|
|
1539
1737
|
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1540
1738
|
try {
|
|
1541
|
-
if (!(0,
|
|
1542
|
-
const content = await (0,
|
|
1739
|
+
if (!(0, import_node_fs3.existsSync)(metaPath)) return null;
|
|
1740
|
+
const content = await (0, import_promises3.readFile)(metaPath, "utf-8");
|
|
1543
1741
|
return JSON.parse(content);
|
|
1544
1742
|
} catch {
|
|
1545
1743
|
return null;
|
|
@@ -1547,7 +1745,7 @@ async function loadVoiceoverMeta(outputDir) {
|
|
|
1547
1745
|
}
|
|
1548
1746
|
async function saveVoiceoverMeta(outputDir, meta) {
|
|
1549
1747
|
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1550
|
-
await (0,
|
|
1748
|
+
await (0, import_promises3.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
1551
1749
|
}
|
|
1552
1750
|
async function concatenateAudio(inputs, output, workDir) {
|
|
1553
1751
|
const formatPath = (f) => {
|
|
@@ -1556,17 +1754,17 @@ async function concatenateAudio(inputs, output, workDir) {
|
|
|
1556
1754
|
};
|
|
1557
1755
|
const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
|
|
1558
1756
|
const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
|
|
1559
|
-
await (0,
|
|
1757
|
+
await (0, import_promises3.writeFile)(listPath, listContent);
|
|
1560
1758
|
await concatMedia(listPath, output);
|
|
1561
|
-
await (0,
|
|
1759
|
+
await (0, import_promises3.unlink)(listPath).catch(() => {
|
|
1562
1760
|
});
|
|
1563
1761
|
}
|
|
1564
|
-
var
|
|
1762
|
+
var import_node_fs3, import_promises3, import_node_path2, isWindows;
|
|
1565
1763
|
var init_helpers = __esm({
|
|
1566
1764
|
"src/lib/tts/helpers.ts"() {
|
|
1567
1765
|
"use strict";
|
|
1568
|
-
|
|
1569
|
-
|
|
1766
|
+
import_node_fs3 = require("fs");
|
|
1767
|
+
import_promises3 = require("fs/promises");
|
|
1570
1768
|
import_node_path2 = require("path");
|
|
1571
1769
|
init_ffmpeg2();
|
|
1572
1770
|
init_utils();
|
|
@@ -1615,9 +1813,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1615
1813
|
const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1616
1814
|
const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1617
1815
|
const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1618
|
-
if ((0,
|
|
1816
|
+
if ((0, import_node_fs4.existsSync)(audioPath)) {
|
|
1619
1817
|
segments.push(audioPath);
|
|
1620
|
-
if ((0,
|
|
1818
|
+
if ((0, import_node_fs4.existsSync)(originalPaddingPath)) {
|
|
1621
1819
|
segments.push(originalPaddingPath);
|
|
1622
1820
|
}
|
|
1623
1821
|
if (recordingDuration > voiceDuration) {
|
|
@@ -1625,7 +1823,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1625
1823
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1626
1824
|
segments.push(alignPaddingPath);
|
|
1627
1825
|
}
|
|
1628
|
-
} else if ((0,
|
|
1826
|
+
} else if ((0, import_node_fs4.existsSync)(silencePath)) {
|
|
1629
1827
|
if (targetDuration !== voiceDuration) {
|
|
1630
1828
|
await generateSilence(silencePath, targetDuration);
|
|
1631
1829
|
}
|
|
@@ -1649,11 +1847,11 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1649
1847
|
}
|
|
1650
1848
|
return alignedStepAudio;
|
|
1651
1849
|
}
|
|
1652
|
-
var
|
|
1850
|
+
var import_node_fs4, import_node_path3;
|
|
1653
1851
|
var init_timing = __esm({
|
|
1654
1852
|
"src/lib/tts/timing.ts"() {
|
|
1655
1853
|
"use strict";
|
|
1656
|
-
|
|
1854
|
+
import_node_fs4 = require("fs");
|
|
1657
1855
|
import_node_path3 = require("path");
|
|
1658
1856
|
init_ffmpeg2();
|
|
1659
1857
|
init_helpers();
|
|
@@ -1718,6 +1916,19 @@ var init_voices = __esm({
|
|
|
1718
1916
|
|
|
1719
1917
|
// src/lib/tts/synthesize.ts
|
|
1720
1918
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1919
|
+
let pythonMsg = "";
|
|
1920
|
+
for (let attempt = 0; attempt < PYTHON_ATTEMPTS; attempt++) {
|
|
1921
|
+
try {
|
|
1922
|
+
await synthesizeWithPython(text, outputPath, voice);
|
|
1923
|
+
return;
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
pythonMsg = err instanceof Error ? err.message : String(err);
|
|
1926
|
+
if (attempt < PYTHON_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 500));
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
await synthesizeWithNode(text, outputPath, voice, pythonMsg);
|
|
1930
|
+
}
|
|
1931
|
+
async function synthesizeWithPython(text, outputPath, voice) {
|
|
1721
1932
|
const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1722
1933
|
const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
|
|
1723
1934
|
const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
|
|
@@ -1735,44 +1946,49 @@ async def main():
|
|
|
1735
1946
|
asyncio.run(main())
|
|
1736
1947
|
`;
|
|
1737
1948
|
try {
|
|
1738
|
-
await (0,
|
|
1949
|
+
await (0, import_promises4.writeFile)(tempScriptPath, pythonScript);
|
|
1739
1950
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1740
1951
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1952
|
+
} finally {
|
|
1953
|
+
await (0, import_promises4.unlink)(tempScriptPath).catch(() => {
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
async function synthesizeWithNode(text, outputPath, voice, pythonMsg, maxRetries = 2) {
|
|
1958
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1959
|
+
try {
|
|
1960
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
|
|
1961
|
+
const tts = new MsEdgeTTS();
|
|
1962
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
|
|
1963
|
+
const { audioStream } = tts.toStream(text);
|
|
1964
|
+
await (0, import_promises5.pipeline)(audioStream, (0, import_node_fs5.createWriteStream)(outputPath));
|
|
1965
|
+
const { size } = await (0, import_promises4.stat)(outputPath);
|
|
1966
|
+
if (size === 0) throw new Error("No audio data received");
|
|
1967
|
+
return;
|
|
1968
|
+
} catch (nodeErr) {
|
|
1969
|
+
await (0, import_promises4.unlink)(outputPath).catch(() => {
|
|
1970
|
+
});
|
|
1971
|
+
if (attempt === maxRetries) {
|
|
1972
|
+
const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
1973
|
+
throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
|
|
1762
1974
|
}
|
|
1975
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1763
1976
|
}
|
|
1764
1977
|
}
|
|
1765
1978
|
}
|
|
1766
|
-
var
|
|
1979
|
+
var import_node_child_process3, import_node_fs5, import_promises4, import_node_path5, import_promises5, import_node_util, execAsync, PYTHON_ATTEMPTS;
|
|
1767
1980
|
var init_synthesize = __esm({
|
|
1768
1981
|
"src/lib/tts/synthesize.ts"() {
|
|
1769
1982
|
"use strict";
|
|
1770
|
-
|
|
1771
|
-
|
|
1983
|
+
import_node_child_process3 = require("child_process");
|
|
1984
|
+
import_node_fs5 = require("fs");
|
|
1985
|
+
import_promises4 = require("fs/promises");
|
|
1772
1986
|
import_node_path5 = require("path");
|
|
1987
|
+
import_promises5 = require("stream/promises");
|
|
1773
1988
|
import_node_util = require("util");
|
|
1774
1989
|
init_helpers();
|
|
1775
|
-
execAsync = (0, import_node_util.promisify)(
|
|
1990
|
+
execAsync = (0, import_node_util.promisify)(import_node_child_process3.exec);
|
|
1991
|
+
PYTHON_ATTEMPTS = 2;
|
|
1776
1992
|
}
|
|
1777
1993
|
});
|
|
1778
1994
|
|
|
@@ -1809,7 +2025,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1809
2025
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1810
2026
|
}
|
|
1811
2027
|
const audioDir = (0, import_node_path6.join)(workDir, "audio");
|
|
1812
|
-
await (0,
|
|
2028
|
+
await (0, import_promises6.mkdir)(audioDir, { recursive: true });
|
|
1813
2029
|
const segments = [];
|
|
1814
2030
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1815
2031
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1860,7 +2076,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1860
2076
|
return { voiceoverPath: "", stepAudio };
|
|
1861
2077
|
}
|
|
1862
2078
|
const concatTask = task2("Combining audio");
|
|
1863
|
-
await (0,
|
|
2079
|
+
await (0, import_promises6.mkdir)(outputDir, { recursive: true });
|
|
1864
2080
|
const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
|
|
1865
2081
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1866
2082
|
concatTask.succeed("Done");
|
|
@@ -1874,11 +2090,11 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1874
2090
|
await saveVoiceoverMeta(outputDir, meta);
|
|
1875
2091
|
return { voiceoverPath, stepAudio };
|
|
1876
2092
|
}
|
|
1877
|
-
var
|
|
2093
|
+
var import_promises6, import_node_path6;
|
|
1878
2094
|
var init_generate = __esm({
|
|
1879
2095
|
"src/lib/tts/generate.ts"() {
|
|
1880
2096
|
"use strict";
|
|
1881
|
-
|
|
2097
|
+
import_promises6 = require("fs/promises");
|
|
1882
2098
|
import_node_path6 = require("path");
|
|
1883
2099
|
init_ffmpeg2();
|
|
1884
2100
|
init_voices();
|
|
@@ -1925,6 +2141,7 @@ var init_types3 = __esm({
|
|
|
1925
2141
|
// src/lib/index.ts
|
|
1926
2142
|
var lib_exports = {};
|
|
1927
2143
|
__export(lib_exports, {
|
|
2144
|
+
CLI_BACKOFF_MS: () => CLI_BACKOFF_MS,
|
|
1928
2145
|
CONFIG_FILE: () => CONFIG_FILE,
|
|
1929
2146
|
CURSOR_CSS: () => CURSOR_CSS,
|
|
1930
2147
|
CURSOR_PNG: () => CURSOR_PNG,
|
|
@@ -1935,9 +2152,11 @@ __export(lib_exports, {
|
|
|
1935
2152
|
FEATURES: () => FEATURES,
|
|
1936
2153
|
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1937
2154
|
LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
|
|
2155
|
+
SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
|
|
1938
2156
|
VERSION: () => VERSION,
|
|
1939
2157
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1940
2158
|
applyTransitions: () => applyTransitions,
|
|
2159
|
+
buildConcatArgs: () => buildConcatArgs,
|
|
1941
2160
|
buildKeepSegments: () => buildKeepSegments,
|
|
1942
2161
|
buildStepTransitions: () => buildStepTransitions,
|
|
1943
2162
|
buildSystemPrompt: () => buildSystemPrompt,
|
|
@@ -1951,9 +2170,11 @@ __export(lib_exports, {
|
|
|
1951
2170
|
concatVideoWithOutro: () => concatVideoWithOutro,
|
|
1952
2171
|
crawlPage: () => crawlPage,
|
|
1953
2172
|
estimateWordTimings: () => estimateWordTimings,
|
|
2173
|
+
extractJsonCandidates: () => extractJsonCandidates,
|
|
1954
2174
|
generateASSHeader: () => generateASSHeader,
|
|
1955
2175
|
generateSilence: () => generateSilence,
|
|
1956
2176
|
generateVoiceover: () => generateVoiceover,
|
|
2177
|
+
getAudioCodec: () => getAudioCodec,
|
|
1957
2178
|
getErrorMessage: () => getErrorMessage,
|
|
1958
2179
|
getLanguageName: () => getLanguageName,
|
|
1959
2180
|
getMediaDuration: () => getMediaDuration,
|
|
@@ -1964,20 +2185,25 @@ __export(lib_exports, {
|
|
|
1964
2185
|
hasAccess: () => hasAccess,
|
|
1965
2186
|
hasTrialRemaining: () => hasTrialRemaining,
|
|
1966
2187
|
hexToASS: () => import_video_kit.hexToAss,
|
|
2188
|
+
idSelector: () => idSelector,
|
|
1967
2189
|
isCanceling: () => isCanceling,
|
|
1968
2190
|
isExcludedLink: () => isExcludedLink,
|
|
2191
|
+
isHonestWav: () => isHonestWav,
|
|
1969
2192
|
isPaid: () => isPaid,
|
|
1970
2193
|
isVerified: () => isVerified,
|
|
1971
2194
|
loadExistingVoiceover: () => loadExistingVoiceover,
|
|
1972
2195
|
loadVoiceoverMeta: () => loadVoiceoverMeta,
|
|
1973
2196
|
mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
|
|
1974
2197
|
normalizeUrl: () => normalizeUrl,
|
|
2198
|
+
parseAIJson: () => parseAIJson,
|
|
1975
2199
|
parseAIPlanResponse: () => parseAIPlanResponse,
|
|
1976
2200
|
parsePartial: () => parsePartial,
|
|
1977
2201
|
pricingUrl: () => pricingUrl,
|
|
1978
2202
|
registerFreeUser: () => registerFreeUser,
|
|
2203
|
+
repairUnicodeEscapes: () => repairUnicodeEscapes,
|
|
1979
2204
|
resolveVoice: () => resolveVoice,
|
|
1980
2205
|
rgbaToHex: () => rgbaToHex,
|
|
2206
|
+
salvageTruncatedJson: () => salvageTruncatedJson,
|
|
1981
2207
|
shouldSkipLink: () => shouldSkipLink,
|
|
1982
2208
|
shouldWatermark: () => shouldWatermark,
|
|
1983
2209
|
showPaywall: () => showPaywall,
|
|
@@ -1996,6 +2222,7 @@ var init_lib = __esm({
|
|
|
1996
2222
|
init_crawl();
|
|
1997
2223
|
init_cursor();
|
|
1998
2224
|
init_ffmpeg2();
|
|
2225
|
+
init_json_repair();
|
|
1999
2226
|
init_prompts();
|
|
2000
2227
|
init_SparkStripe_feature_gating();
|
|
2001
2228
|
init_scraping();
|
|
@@ -2011,19 +2238,17 @@ var init_lib = __esm({
|
|
|
2011
2238
|
|
|
2012
2239
|
// src/qp.ts
|
|
2013
2240
|
var import_dotenv = __toESM(require("dotenv"));
|
|
2014
|
-
var
|
|
2241
|
+
var import_chalk21 = __toESM(require("chalk"));
|
|
2015
2242
|
|
|
2016
2243
|
// src/commands/auth-cmd.ts
|
|
2017
|
-
var import_node_readline2 = require("readline");
|
|
2018
2244
|
var import_chalk3 = __toESM(require("chalk"));
|
|
2019
|
-
var import_playwright2 = require("playwright");
|
|
2020
2245
|
|
|
2021
2246
|
// src/browser/auth-state.ts
|
|
2022
2247
|
var import_node_fs = require("fs");
|
|
2023
2248
|
var import_promises = require("fs/promises");
|
|
2024
2249
|
var import_node_os = require("os");
|
|
2025
2250
|
var import_node_path = require("path");
|
|
2026
|
-
var AUTH_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
|
|
2251
|
+
var AUTH_DIR = process.env.QUICKPEEK_AUTH_DIR || (0, import_node_path.join)((0, import_node_os.homedir)(), ".quickpeek", "auth");
|
|
2027
2252
|
function hostOf(url) {
|
|
2028
2253
|
try {
|
|
2029
2254
|
return new URL(url).host;
|
|
@@ -2035,150 +2260,462 @@ function authStatePath(url) {
|
|
|
2035
2260
|
const host = hostOf(url);
|
|
2036
2261
|
return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
|
|
2037
2262
|
}
|
|
2263
|
+
function authMetaPath(url) {
|
|
2264
|
+
const host = hostOf(url);
|
|
2265
|
+
return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.meta.json`) : null;
|
|
2266
|
+
}
|
|
2038
2267
|
function findAuthState(url) {
|
|
2039
2268
|
const path = authStatePath(url);
|
|
2040
2269
|
return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
|
|
2041
2270
|
}
|
|
2042
|
-
async function saveAuthState(url, state) {
|
|
2271
|
+
async function saveAuthState(url, state, startUrl) {
|
|
2043
2272
|
const path = authStatePath(url);
|
|
2044
2273
|
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2045
2274
|
await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
|
|
2046
2275
|
await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
|
|
2047
2276
|
await (0, import_promises.chmod)(path, 384);
|
|
2277
|
+
if (startUrl) {
|
|
2278
|
+
await (0, import_promises.writeFile)(authMetaPath(url), JSON.stringify({ startUrl, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
2279
|
+
}
|
|
2048
2280
|
return path;
|
|
2049
2281
|
}
|
|
2282
|
+
function findStartUrl(url) {
|
|
2283
|
+
const path = authMetaPath(url);
|
|
2284
|
+
if (!path || !(0, import_node_fs.existsSync)(path)) return void 0;
|
|
2285
|
+
try {
|
|
2286
|
+
const startUrl = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8")).startUrl;
|
|
2287
|
+
return typeof startUrl === "string" && startUrl.startsWith("http") ? startUrl : void 0;
|
|
2288
|
+
} catch {
|
|
2289
|
+
return void 0;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function resolveStartUrl(url) {
|
|
2293
|
+
try {
|
|
2294
|
+
const parsed = new URL(url);
|
|
2295
|
+
const bareOrigin = (parsed.pathname === "/" || parsed.pathname === "") && !parsed.search && !parsed.hash;
|
|
2296
|
+
if (!bareOrigin || !findAuthState(url)) return url;
|
|
2297
|
+
return findStartUrl(url) ?? url;
|
|
2298
|
+
} catch {
|
|
2299
|
+
return url;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2050
2302
|
async function clearAuthState(url) {
|
|
2051
2303
|
if (url) {
|
|
2052
2304
|
const path = authStatePath(url);
|
|
2053
2305
|
if (!path || !(0, import_node_fs.existsSync)(path)) return [];
|
|
2054
2306
|
await (0, import_promises.unlink)(path);
|
|
2307
|
+
const meta = authMetaPath(url);
|
|
2308
|
+
if (meta && (0, import_node_fs.existsSync)(meta)) await (0, import_promises.unlink)(meta);
|
|
2055
2309
|
return [hostOf(url)];
|
|
2056
2310
|
}
|
|
2057
2311
|
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2058
2312
|
const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
|
|
2059
2313
|
await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
|
|
2060
|
-
return files.map((f) => f.slice(0, -".json".length));
|
|
2314
|
+
return files.filter((f) => !f.endsWith(".meta.json")).map((f) => f.slice(0, -".json".length));
|
|
2061
2315
|
}
|
|
2062
2316
|
async function listAuthStates() {
|
|
2063
2317
|
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2064
|
-
return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
|
|
2318
|
+
return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json") && !f.endsWith(".meta.json")).map((f) => f.slice(0, -".json".length));
|
|
2065
2319
|
}
|
|
2066
2320
|
|
|
2067
|
-
// src/
|
|
2068
|
-
var
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
// src/logo.ts
|
|
2072
|
-
var import_figlet = __toESM(require("figlet"));
|
|
2073
|
-
var import_gradient_string = __toESM(require("gradient-string"));
|
|
2074
|
-
var DEFAULT_LOGO_CONFIG = {
|
|
2075
|
-
text: "QuickPeek",
|
|
2076
|
-
font: "Slant",
|
|
2077
|
-
horizontalLayout: "default",
|
|
2078
|
-
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2079
|
-
suffix: " \u{1F440}"
|
|
2080
|
-
};
|
|
2081
|
-
function renderLogo(config = {}) {
|
|
2082
|
-
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2083
|
-
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
2084
|
-
font: resolved.font,
|
|
2085
|
-
horizontalLayout: resolved.horizontalLayout
|
|
2086
|
-
});
|
|
2087
|
-
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
2088
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2089
|
-
}
|
|
2321
|
+
// src/browser/launch.ts
|
|
2322
|
+
var import_promises2 = require("fs/promises");
|
|
2323
|
+
var import_playwright2 = require("playwright");
|
|
2090
2324
|
|
|
2091
|
-
// src/
|
|
2092
|
-
var
|
|
2093
|
-
var
|
|
2094
|
-
var
|
|
2095
|
-
var
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
const config = {
|
|
2104
|
-
lang: langOverride || userConfig.lang || "en",
|
|
2105
|
-
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2106
|
-
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2107
|
-
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2108
|
-
music: userConfig.music,
|
|
2109
|
-
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2110
|
-
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2111
|
-
};
|
|
2112
|
-
return { config, created: false };
|
|
2113
|
-
} catch {
|
|
2114
|
-
}
|
|
2325
|
+
// src/browser/wsl.ts
|
|
2326
|
+
var import_node_child_process = require("child_process");
|
|
2327
|
+
var import_node_fs2 = require("fs");
|
|
2328
|
+
var import_node_http = require("http");
|
|
2329
|
+
var import_playwright = require("playwright");
|
|
2330
|
+
function isWSL() {
|
|
2331
|
+
if (process.platform !== "linux") return false;
|
|
2332
|
+
try {
|
|
2333
|
+
const r = (0, import_node_child_process.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2334
|
+
return r.includes("microsoft") || r.includes("wsl");
|
|
2335
|
+
} catch {
|
|
2336
|
+
return false;
|
|
2115
2337
|
}
|
|
2116
|
-
const fullDefaultConfig = {
|
|
2117
|
-
lang: "en",
|
|
2118
|
-
plan: { maxSteps: 8 },
|
|
2119
|
-
video: {
|
|
2120
|
-
width: 1080,
|
|
2121
|
-
height: 1920,
|
|
2122
|
-
fps: 30,
|
|
2123
|
-
quality: 0.5,
|
|
2124
|
-
preset: "fast",
|
|
2125
|
-
cursor: true,
|
|
2126
|
-
highlight: false,
|
|
2127
|
-
fadeIn: 500,
|
|
2128
|
-
// ms
|
|
2129
|
-
fadeOut: 500,
|
|
2130
|
-
// ms
|
|
2131
|
-
contrast: 0.25
|
|
2132
|
-
},
|
|
2133
|
-
audio: {
|
|
2134
|
-
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2135
|
-
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2136
|
-
speed: 1
|
|
2137
|
-
},
|
|
2138
|
-
music: {
|
|
2139
|
-
path: "",
|
|
2140
|
-
volume: 0.3
|
|
2141
|
-
},
|
|
2142
|
-
output: { dir: "./demo" }
|
|
2143
|
-
};
|
|
2144
|
-
await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2145
|
-
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2146
2338
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
|
|
2150
|
-
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2151
|
-
await (0, import_promises5.rm)(workDir, { recursive: true });
|
|
2152
|
-
}
|
|
2153
|
-
await (0, import_promises5.mkdir)(workDir, { recursive: true });
|
|
2154
|
-
return workDir;
|
|
2339
|
+
function tryEnableWSLInterop() {
|
|
2340
|
+
return (0, import_node_fs2.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2155
2341
|
}
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2342
|
+
function wslToWindowsPath(wslPath) {
|
|
2343
|
+
if (!wslPath) return wslPath;
|
|
2344
|
+
const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
|
|
2345
|
+
if (!m) return wslPath;
|
|
2346
|
+
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2347
|
+
}
|
|
2348
|
+
function findExe(whereExeQuery, ...hardcoded) {
|
|
2349
|
+
for (const p of hardcoded) if ((0, import_node_fs2.existsSync)(p)) return p;
|
|
2159
2350
|
try {
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
}
|
|
2351
|
+
const line = (0, import_node_child_process.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2352
|
+
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2163
2353
|
} catch {
|
|
2164
2354
|
}
|
|
2165
|
-
return
|
|
2355
|
+
return null;
|
|
2166
2356
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2357
|
+
var findWindowsChrome = () => findExe(
|
|
2358
|
+
"chrome.exe",
|
|
2359
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2360
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
|
2361
|
+
);
|
|
2362
|
+
var findWindowsNode = () => findExe("node.exe");
|
|
2363
|
+
function getWindowsHostIP() {
|
|
2364
|
+
try {
|
|
2365
|
+
const m = (0, import_node_child_process.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2366
|
+
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2367
|
+
);
|
|
2368
|
+
if (m?.[1]) return m[1];
|
|
2369
|
+
} catch {
|
|
2170
2370
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2371
|
+
try {
|
|
2372
|
+
const m = (0, import_node_fs2.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2373
|
+
if (m?.[1]) return m[1];
|
|
2374
|
+
} catch {
|
|
2375
|
+
}
|
|
2376
|
+
return "127.0.0.1";
|
|
2174
2377
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2378
|
+
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2379
|
+
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')});`;
|
|
2380
|
+
return new Promise((resolve9, reject) => {
|
|
2381
|
+
const proc = (0, import_node_child_process.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2382
|
+
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2383
|
+
let buf = "";
|
|
2384
|
+
proc.stdout.on("data", (d) => {
|
|
2385
|
+
buf += d.toString();
|
|
2386
|
+
const m = buf.match(/PORT:(\d+)/);
|
|
2387
|
+
if (m) {
|
|
2388
|
+
clearTimeout(timer);
|
|
2389
|
+
resolve9({ proc, port: +m[1] });
|
|
2390
|
+
}
|
|
2391
|
+
});
|
|
2392
|
+
proc.on("error", (e) => {
|
|
2393
|
+
clearTimeout(timer);
|
|
2394
|
+
reject(e);
|
|
2395
|
+
});
|
|
2396
|
+
});
|
|
2178
2397
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2398
|
+
function waitForChrome(host, port) {
|
|
2399
|
+
return new Promise((resolve9, reject) => {
|
|
2400
|
+
const deadline = Date.now() + 2e4;
|
|
2401
|
+
const poll = () => {
|
|
2402
|
+
if (Date.now() > deadline) {
|
|
2403
|
+
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const req = (0, import_node_http.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2407
|
+
res.resume();
|
|
2408
|
+
resolve9();
|
|
2409
|
+
});
|
|
2410
|
+
req.on("error", () => setTimeout(poll, 600));
|
|
2411
|
+
req.setTimeout(1e3, () => {
|
|
2412
|
+
req.destroy();
|
|
2413
|
+
setTimeout(poll, 600);
|
|
2414
|
+
});
|
|
2415
|
+
req.end();
|
|
2416
|
+
};
|
|
2417
|
+
poll();
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
2421
|
+
const debugPort = options.debugPort ?? 9222;
|
|
2422
|
+
const chromePath = findWindowsChrome();
|
|
2423
|
+
if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
|
|
2424
|
+
const nodePath = findWindowsNode();
|
|
2425
|
+
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2426
|
+
const winUserData = wslToWindowsPath(userDataDir);
|
|
2427
|
+
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2428
|
+
const chromeProc = (0, import_node_child_process.spawn)(
|
|
2429
|
+
chromePath,
|
|
2430
|
+
[
|
|
2431
|
+
`--remote-debugging-port=${debugPort}`,
|
|
2432
|
+
`--user-data-dir=${winUserData}`,
|
|
2433
|
+
...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
|
|
2434
|
+
"--no-first-run",
|
|
2435
|
+
"--no-default-browser-check",
|
|
2436
|
+
"--disable-background-networking",
|
|
2437
|
+
"--disable-sync",
|
|
2438
|
+
"--disable-features=TranslateUI",
|
|
2439
|
+
options.startMaximized ? "--start-maximized" : "--start-minimized"
|
|
2440
|
+
],
|
|
2441
|
+
{ stdio: "ignore", detached: true }
|
|
2442
|
+
);
|
|
2443
|
+
chromeProc.unref();
|
|
2444
|
+
const windowsHost = getWindowsHostIP();
|
|
2445
|
+
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2446
|
+
try {
|
|
2447
|
+
await waitForChrome(windowsHost, proxyPort);
|
|
2448
|
+
const browser = await import_playwright.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2449
|
+
return {
|
|
2450
|
+
browser,
|
|
2451
|
+
cleanup: async () => {
|
|
2452
|
+
try {
|
|
2453
|
+
await browser.close();
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
relay.kill();
|
|
2457
|
+
}
|
|
2458
|
+
};
|
|
2459
|
+
} catch (err) {
|
|
2460
|
+
relay.kill();
|
|
2461
|
+
throw err;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/browser/launch.ts
|
|
2466
|
+
function recordingViewport(config) {
|
|
2467
|
+
const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
|
|
2468
|
+
return {
|
|
2469
|
+
width: Math.round(config.video.width / zoom),
|
|
2470
|
+
height: Math.round(config.video.height / zoom)
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
2474
|
+
const extensionPath = config.browser?.extension;
|
|
2475
|
+
const zoom = config.video.zoom && config.video.zoom > 0 ? config.video.zoom : 1;
|
|
2476
|
+
const viewport = recordingViewport(config);
|
|
2477
|
+
const scaleOpts = zoom === 1 ? {} : { deviceScaleFactor: zoom };
|
|
2478
|
+
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
2479
|
+
const cspOpts = { bypassCSP: true };
|
|
2480
|
+
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
2481
|
+
let context;
|
|
2482
|
+
let browser = null;
|
|
2483
|
+
let chromeSession = null;
|
|
2484
|
+
if (extensionPath) {
|
|
2485
|
+
await (0, import_promises2.mkdir)(userDataDir, { recursive: true });
|
|
2486
|
+
const persistentArgs = [
|
|
2487
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
2488
|
+
`--load-extension=${extensionPath}`,
|
|
2489
|
+
"--window-position=-32000,-32000",
|
|
2490
|
+
"--window-size=1,1"
|
|
2491
|
+
];
|
|
2492
|
+
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
2493
|
+
if (useWindowsChrome) {
|
|
2494
|
+
try {
|
|
2495
|
+
chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...scaleOpts, ...videoOpts });
|
|
2496
|
+
const contexts = chromeSession.browser.contexts();
|
|
2497
|
+
context = contexts[0] || await chromeSession.browser.newContext({
|
|
2498
|
+
viewport,
|
|
2499
|
+
...scaleOpts,
|
|
2500
|
+
...videoOpts,
|
|
2501
|
+
...cspOpts,
|
|
2502
|
+
acceptDownloads: true
|
|
2503
|
+
});
|
|
2504
|
+
} catch {
|
|
2505
|
+
context = await import_playwright2.chromium.launchPersistentContext(userDataDir, {
|
|
2506
|
+
headless: false,
|
|
2507
|
+
args: persistentArgs,
|
|
2508
|
+
viewport,
|
|
2509
|
+
...scaleOpts,
|
|
2510
|
+
...videoOpts,
|
|
2511
|
+
...cspOpts,
|
|
2512
|
+
acceptDownloads: true
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
context = await import_playwright2.chromium.launchPersistentContext(userDataDir, {
|
|
2517
|
+
headless: false,
|
|
2518
|
+
args: persistentArgs,
|
|
2519
|
+
viewport,
|
|
2520
|
+
...scaleOpts,
|
|
2521
|
+
...videoOpts,
|
|
2522
|
+
...cspOpts,
|
|
2523
|
+
acceptDownloads: true
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
if (process.env.QP_HEADED === "1") {
|
|
2528
|
+
const headedOpts = {
|
|
2529
|
+
headless: false,
|
|
2530
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
2531
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
2532
|
+
};
|
|
2533
|
+
try {
|
|
2534
|
+
browser = await import_playwright2.chromium.launch({ ...headedOpts, channel: "chrome" });
|
|
2535
|
+
} catch {
|
|
2536
|
+
browser = await import_playwright2.chromium.launch(headedOpts);
|
|
2537
|
+
}
|
|
2538
|
+
} else {
|
|
2539
|
+
browser = await import_playwright2.chromium.launch({ headless: true });
|
|
2540
|
+
}
|
|
2541
|
+
context = await browser.newContext({
|
|
2542
|
+
viewport,
|
|
2543
|
+
...scaleOpts,
|
|
2544
|
+
...videoOpts,
|
|
2545
|
+
...authOpts,
|
|
2546
|
+
...cspOpts,
|
|
2547
|
+
acceptDownloads: true
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
await hideDevOverlays(context);
|
|
2551
|
+
return { context, browser, chromeSession };
|
|
2552
|
+
}
|
|
2553
|
+
async function hideDevOverlays(context) {
|
|
2554
|
+
await context.addInitScript(() => {
|
|
2555
|
+
const css = "nextjs-portal,vite-error-overlay,vite-plugin-checker-error-overlay{display:none!important;pointer-events:none!important}";
|
|
2556
|
+
const add = () => {
|
|
2557
|
+
const style = document.createElement("style");
|
|
2558
|
+
style.textContent = css;
|
|
2559
|
+
document.head?.appendChild(style);
|
|
2560
|
+
};
|
|
2561
|
+
if (document.head) add();
|
|
2562
|
+
else document.addEventListener("DOMContentLoaded", add, { once: true });
|
|
2563
|
+
}).catch(() => {
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
async function launchSignInBrowser() {
|
|
2567
|
+
const opts = {
|
|
2568
|
+
headless: false,
|
|
2569
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
2570
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
2571
|
+
};
|
|
2572
|
+
try {
|
|
2573
|
+
return await import_playwright2.chromium.launch({ ...opts, channel: "chrome" });
|
|
2574
|
+
} catch {
|
|
2575
|
+
return await import_playwright2.chromium.launch(opts);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
async function cleanupBrowserSession(session) {
|
|
2579
|
+
if (session.chromeSession) {
|
|
2580
|
+
await session.chromeSession.cleanup();
|
|
2581
|
+
} else {
|
|
2582
|
+
await session.context.close();
|
|
2583
|
+
if (session.browser) await session.browser.close();
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// src/browser/auth-session.ts
|
|
2588
|
+
function sameSite(a, b) {
|
|
2589
|
+
if (a === b) return true;
|
|
2590
|
+
const tail = (h) => h.split(".").slice(-2).join(".");
|
|
2591
|
+
return a.includes(".") && b.includes(".") && tail(a) === tail(b);
|
|
2592
|
+
}
|
|
2593
|
+
function captureStartUrl(context, targetUrl) {
|
|
2594
|
+
const targetHost = new URL(targetUrl).host;
|
|
2595
|
+
const pages = context.pages();
|
|
2596
|
+
for (let i = pages.length - 1; i >= 0; i--) {
|
|
2597
|
+
const pageUrl = pages[i].url();
|
|
2598
|
+
if (!pageUrl.startsWith("http")) continue;
|
|
2599
|
+
if (sameSite(new URL(pageUrl).host, targetHost)) return pageUrl;
|
|
2600
|
+
}
|
|
2601
|
+
return void 0;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// src/help.ts
|
|
2605
|
+
var import_chalk = __toESM(require("chalk"));
|
|
2606
|
+
init_lib();
|
|
2607
|
+
|
|
2608
|
+
// src/logo.ts
|
|
2609
|
+
var import_figlet = __toESM(require("figlet"));
|
|
2610
|
+
var import_gradient_string = __toESM(require("gradient-string"));
|
|
2611
|
+
var DEFAULT_LOGO_CONFIG = {
|
|
2612
|
+
text: "QuickPeek",
|
|
2613
|
+
font: "Slant",
|
|
2614
|
+
horizontalLayout: "default",
|
|
2615
|
+
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2616
|
+
suffix: " \u{1F440}"
|
|
2617
|
+
};
|
|
2618
|
+
function renderLogo(config = {}) {
|
|
2619
|
+
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2620
|
+
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
2621
|
+
font: resolved.font,
|
|
2622
|
+
horizontalLayout: resolved.horizontalLayout
|
|
2623
|
+
});
|
|
2624
|
+
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
2625
|
+
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// src/utils/config-loader.ts
|
|
2629
|
+
var import_node_fs6 = require("fs");
|
|
2630
|
+
var import_promises7 = require("fs/promises");
|
|
2631
|
+
var import_node_os2 = require("os");
|
|
2632
|
+
var import_node_path7 = require("path");
|
|
2633
|
+
init_lib();
|
|
2634
|
+
async function loadConfig(_url, langOverride) {
|
|
2635
|
+
const configPath = (0, import_node_path7.join)(process.cwd(), CONFIG_FILE);
|
|
2636
|
+
if ((0, import_node_fs6.existsSync)(configPath)) {
|
|
2637
|
+
try {
|
|
2638
|
+
const content = await (0, import_promises7.readFile)(configPath, "utf-8");
|
|
2639
|
+
const userConfig = JSON.parse(content);
|
|
2640
|
+
const config = {
|
|
2641
|
+
lang: langOverride || userConfig.lang || "en",
|
|
2642
|
+
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2643
|
+
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2644
|
+
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2645
|
+
music: userConfig.music,
|
|
2646
|
+
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2647
|
+
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2648
|
+
};
|
|
2649
|
+
return { config, created: false };
|
|
2650
|
+
} catch {
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
const fullDefaultConfig = {
|
|
2654
|
+
lang: "en",
|
|
2655
|
+
plan: { maxSteps: 8 },
|
|
2656
|
+
video: {
|
|
2657
|
+
width: 1080,
|
|
2658
|
+
height: 1920,
|
|
2659
|
+
fps: 30,
|
|
2660
|
+
quality: 0.5,
|
|
2661
|
+
preset: "fast",
|
|
2662
|
+
cursor: true,
|
|
2663
|
+
highlight: false,
|
|
2664
|
+
fadeIn: 500,
|
|
2665
|
+
// ms
|
|
2666
|
+
fadeOut: 500,
|
|
2667
|
+
// ms
|
|
2668
|
+
contrast: 0.25
|
|
2669
|
+
},
|
|
2670
|
+
audio: {
|
|
2671
|
+
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2672
|
+
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2673
|
+
speed: 1
|
|
2674
|
+
},
|
|
2675
|
+
music: {
|
|
2676
|
+
path: "",
|
|
2677
|
+
volume: 0.3
|
|
2678
|
+
},
|
|
2679
|
+
output: { dir: "./demo" }
|
|
2680
|
+
};
|
|
2681
|
+
await (0, import_promises7.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2682
|
+
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2683
|
+
}
|
|
2684
|
+
var WORK_DIR = ".quickpeek";
|
|
2685
|
+
async function prepareWorkDir() {
|
|
2686
|
+
const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
|
|
2687
|
+
if ((0, import_node_fs6.existsSync)(workDir)) {
|
|
2688
|
+
await (0, import_promises7.rm)(workDir, { recursive: true });
|
|
2689
|
+
}
|
|
2690
|
+
await (0, import_promises7.mkdir)(workDir, { recursive: true });
|
|
2691
|
+
return workDir;
|
|
2692
|
+
}
|
|
2693
|
+
var USER_DATA_DIR = (0, import_node_path7.join)((0, import_node_os2.homedir)(), ".quickpeek");
|
|
2694
|
+
var USER_DATA_FILE = (0, import_node_path7.join)(USER_DATA_DIR, "user.json");
|
|
2695
|
+
async function loadUserData() {
|
|
2696
|
+
try {
|
|
2697
|
+
if ((0, import_node_fs6.existsSync)(USER_DATA_FILE)) {
|
|
2698
|
+
return JSON.parse(await (0, import_promises7.readFile)(USER_DATA_FILE, "utf-8"));
|
|
2699
|
+
}
|
|
2700
|
+
} catch {
|
|
2701
|
+
}
|
|
2702
|
+
return {};
|
|
2703
|
+
}
|
|
2704
|
+
async function saveUserData(userData) {
|
|
2705
|
+
if (!(0, import_node_fs6.existsSync)(USER_DATA_DIR)) {
|
|
2706
|
+
await (0, import_promises7.mkdir)(USER_DATA_DIR, { recursive: true });
|
|
2707
|
+
}
|
|
2708
|
+
const existing = await loadUserData();
|
|
2709
|
+
const merged = { ...existing, ...userData };
|
|
2710
|
+
await (0, import_promises7.writeFile)(USER_DATA_FILE, JSON.stringify(merged, null, 2));
|
|
2711
|
+
}
|
|
2712
|
+
var DEFAULT_CAPTIONS_CSS = `.caption {
|
|
2713
|
+
font-family: Arial, sans-serif;
|
|
2714
|
+
-webkit-text-stroke: 3px #000000;
|
|
2715
|
+
}
|
|
2716
|
+
.caption .regular {
|
|
2717
|
+
font-size: 56px;
|
|
2718
|
+
color: #FFFFFF;
|
|
2182
2719
|
}
|
|
2183
2720
|
.caption .highlight {
|
|
2184
2721
|
font-size: 64px;
|
|
@@ -2232,12 +2769,12 @@ function parseCaptionCSS(css) {
|
|
|
2232
2769
|
}
|
|
2233
2770
|
async function loadCaptions() {
|
|
2234
2771
|
const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
|
|
2235
|
-
if (!(0,
|
|
2236
|
-
await (0,
|
|
2772
|
+
if (!(0, import_node_fs6.existsSync)(captionsPath)) {
|
|
2773
|
+
await (0, import_promises7.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2237
2774
|
return DEFAULT_CAPTIONS;
|
|
2238
2775
|
}
|
|
2239
2776
|
try {
|
|
2240
|
-
const css = await (0,
|
|
2777
|
+
const css = await (0, import_promises7.readFile)(captionsPath, "utf-8");
|
|
2241
2778
|
return parseCaptionCSS(css);
|
|
2242
2779
|
} catch {
|
|
2243
2780
|
return DEFAULT_CAPTIONS;
|
|
@@ -2248,8 +2785,8 @@ async function savePlan(plan, planPath) {
|
|
|
2248
2785
|
...plan,
|
|
2249
2786
|
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2250
2787
|
};
|
|
2251
|
-
await (0,
|
|
2252
|
-
await (0,
|
|
2788
|
+
await (0, import_promises7.mkdir)((0, import_node_path7.dirname)(planPath), { recursive: true });
|
|
2789
|
+
await (0, import_promises7.writeFile)(planPath, JSON.stringify(cleanPlan, null, 2));
|
|
2253
2790
|
}
|
|
2254
2791
|
|
|
2255
2792
|
// src/help.ts
|
|
@@ -2324,9 +2861,11 @@ async function showHelp() {
|
|
|
2324
2861
|
"Use your own narration script"
|
|
2325
2862
|
],
|
|
2326
2863
|
[`${cmd("qp capture")} ${arg("<url>")}`, `qp capture <url>`, "Record interactions manually"],
|
|
2864
|
+
[`${cmd("qp edit")} ${arg("<url>")}`, `qp edit <url>`, "Edit the demo plan in the browser"],
|
|
2327
2865
|
[`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
|
|
2328
2866
|
[`${cmd("qp logout")}`, `qp logout`, "Sign out"],
|
|
2329
2867
|
[`${cmd("qp auth")} ${arg("<url>")}`, `qp auth <url>`, "Save a signed-in session for a site you demo"],
|
|
2868
|
+
[`${cmd("qp auth list")}`, `qp auth list`, "Show which sites have a saved session"],
|
|
2330
2869
|
[`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
|
|
2331
2870
|
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
2332
2871
|
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
@@ -2381,6 +2920,9 @@ var autoYes = false;
|
|
|
2381
2920
|
function setAutoYes(value) {
|
|
2382
2921
|
autoYes = value;
|
|
2383
2922
|
}
|
|
2923
|
+
function isAutoYes() {
|
|
2924
|
+
return autoYes;
|
|
2925
|
+
}
|
|
2384
2926
|
function task(text) {
|
|
2385
2927
|
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2386
2928
|
}
|
|
@@ -2411,10 +2953,19 @@ function promptInput(prompt2) {
|
|
|
2411
2953
|
});
|
|
2412
2954
|
});
|
|
2413
2955
|
}
|
|
2956
|
+
function waitForEnter(prompt2 = `${INDENT}Press Enter when done...`) {
|
|
2957
|
+
return new Promise((resolve9) => {
|
|
2958
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2959
|
+
rl.question(prompt2, () => {
|
|
2960
|
+
rl.close();
|
|
2961
|
+
resolve9();
|
|
2962
|
+
});
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2414
2965
|
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2415
2966
|
function formatClickablePath(filePath) {
|
|
2416
2967
|
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
2417
|
-
if (_isWSL) {
|
|
2968
|
+
if (_isWSL || process.platform !== "win32") {
|
|
2418
2969
|
return absolutePath;
|
|
2419
2970
|
}
|
|
2420
2971
|
let formatted = absolutePath;
|
|
@@ -2431,15 +2982,6 @@ function formatDuration(ms) {
|
|
|
2431
2982
|
}
|
|
2432
2983
|
|
|
2433
2984
|
// src/commands/auth-cmd.ts
|
|
2434
|
-
function waitForEnter(prompt2) {
|
|
2435
|
-
return new Promise((resolve9) => {
|
|
2436
|
-
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2437
|
-
rl.question(prompt2, () => {
|
|
2438
|
-
rl.close();
|
|
2439
|
-
resolve9();
|
|
2440
|
-
});
|
|
2441
|
-
});
|
|
2442
|
-
}
|
|
2443
2985
|
async function authCommand(url) {
|
|
2444
2986
|
console.log();
|
|
2445
2987
|
showLogo();
|
|
@@ -2447,9 +2989,10 @@ async function authCommand(url) {
|
|
|
2447
2989
|
console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
|
|
2448
2990
|
return;
|
|
2449
2991
|
}
|
|
2450
|
-
console.log(import_chalk3.default.white(" A browser will open. Sign in
|
|
2992
|
+
console.log(import_chalk3.default.white(" A browser will open. Sign in, then leave it on the page demos"));
|
|
2993
|
+
console.log(import_chalk3.default.white(" should start from (usually your dashboard) and come back here."));
|
|
2451
2994
|
console.log();
|
|
2452
|
-
const browser = await
|
|
2995
|
+
const browser = await launchSignInBrowser();
|
|
2453
2996
|
const context = await browser.newContext();
|
|
2454
2997
|
const page = await context.newPage();
|
|
2455
2998
|
try {
|
|
@@ -2459,11 +3002,15 @@ async function authCommand(url) {
|
|
|
2459
3002
|
await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
|
|
2460
3003
|
try {
|
|
2461
3004
|
const state = await context.storageState();
|
|
2462
|
-
const
|
|
3005
|
+
const startUrl = captureStartUrl(context, url);
|
|
3006
|
+
const path = await saveAuthState(url, state, startUrl);
|
|
2463
3007
|
console.log();
|
|
2464
3008
|
console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
|
|
2465
3009
|
console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
|
|
2466
3010
|
console.log(import_chalk3.default.dim(" Crawls and recordings of this site now start signed in."));
|
|
3011
|
+
if (startUrl && startUrl !== url) {
|
|
3012
|
+
console.log(import_chalk3.default.dim(" Demos of the bare URL will start at ") + import_chalk3.default.cyan(startUrl));
|
|
3013
|
+
}
|
|
2467
3014
|
console.log(import_chalk3.default.dim(" Run ") + import_chalk3.default.cyan(`qp auth clear ${new URL(url).host}`) + import_chalk3.default.dim(" to forget it."));
|
|
2468
3015
|
} finally {
|
|
2469
3016
|
await browser.close().catch(() => {
|
|
@@ -2505,24 +3052,31 @@ var import_chalk11 = __toESM(require("chalk"));
|
|
|
2505
3052
|
init_lib();
|
|
2506
3053
|
|
|
2507
3054
|
// src/ai/client.ts
|
|
2508
|
-
var
|
|
3055
|
+
var import_node_readline2 = require("readline");
|
|
2509
3056
|
var import_chalk5 = __toESM(require("chalk"));
|
|
2510
3057
|
init_lib();
|
|
2511
3058
|
|
|
2512
3059
|
// src/utils/browser-auth.ts
|
|
2513
|
-
var
|
|
3060
|
+
var import_node_http2 = require("http");
|
|
2514
3061
|
var import_node_url = require("url");
|
|
2515
3062
|
var import_chalk4 = __toESM(require("chalk"));
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
3063
|
+
|
|
3064
|
+
// src/lib/browser-open.ts
|
|
3065
|
+
var import_node_child_process4 = require("child_process");
|
|
3066
|
+
function editorBaseUrl() {
|
|
3067
|
+
return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
|
|
3068
|
+
}
|
|
3069
|
+
function openInBrowser(url) {
|
|
2520
3070
|
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2521
3071
|
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2522
3072
|
return new Promise((resolve9) => {
|
|
2523
|
-
|
|
3073
|
+
(0, import_node_child_process4.exec)(command, () => resolve9());
|
|
2524
3074
|
});
|
|
2525
3075
|
}
|
|
3076
|
+
|
|
3077
|
+
// src/utils/browser-auth.ts
|
|
3078
|
+
init_SparkStripe_feature_gating();
|
|
3079
|
+
var TIMEOUT_MS = 12e4;
|
|
2526
3080
|
async function browserLogin() {
|
|
2527
3081
|
return new Promise((resolve9) => {
|
|
2528
3082
|
let server;
|
|
@@ -2532,7 +3086,7 @@ async function browserLogin() {
|
|
|
2532
3086
|
server?.close();
|
|
2533
3087
|
resolve9(email);
|
|
2534
3088
|
};
|
|
2535
|
-
server = (0,
|
|
3089
|
+
server = (0, import_node_http2.createServer)((req, res) => {
|
|
2536
3090
|
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
2537
3091
|
const path = url.pathname;
|
|
2538
3092
|
if (path === "/callback" || path === "/") {
|
|
@@ -2576,7 +3130,7 @@ async function browserLogin() {
|
|
|
2576
3130
|
console.log(import_chalk4.default.dim(` ${url}`));
|
|
2577
3131
|
console.log();
|
|
2578
3132
|
console.log(import_chalk4.default.dim(" Waiting for sign-in (press Ctrl+C to cancel)..."));
|
|
2579
|
-
|
|
3133
|
+
void openInBrowser(url);
|
|
2580
3134
|
});
|
|
2581
3135
|
timer = setTimeout(() => {
|
|
2582
3136
|
console.log();
|
|
@@ -2586,13 +3140,132 @@ async function browserLogin() {
|
|
|
2586
3140
|
});
|
|
2587
3141
|
}
|
|
2588
3142
|
|
|
3143
|
+
// src/utils/logging.ts
|
|
3144
|
+
var import_promises8 = require("fs/promises");
|
|
3145
|
+
var import_node_path9 = require("path");
|
|
3146
|
+
var LOG_FILE_NAME = "quickpeek.log";
|
|
3147
|
+
var logFileInitialized = false;
|
|
3148
|
+
var logFilePath = LOG_FILE_NAME;
|
|
3149
|
+
async function writeLogEntry(entry) {
|
|
3150
|
+
try {
|
|
3151
|
+
if (!logFileInitialized) {
|
|
3152
|
+
await (0, import_promises8.writeFile)(logFilePath, entry);
|
|
3153
|
+
logFileInitialized = true;
|
|
3154
|
+
} else {
|
|
3155
|
+
await (0, import_promises8.appendFile)(logFilePath, entry);
|
|
3156
|
+
}
|
|
3157
|
+
} catch {
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
async function setLogFilePath(outputDir) {
|
|
3161
|
+
logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
|
|
3162
|
+
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3163
|
+
${"=".repeat(60)}
|
|
3164
|
+
|
|
3165
|
+
`;
|
|
3166
|
+
try {
|
|
3167
|
+
await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(logFilePath), { recursive: true });
|
|
3168
|
+
await (0, import_promises8.writeFile)(logFilePath, header2);
|
|
3169
|
+
logFileInitialized = true;
|
|
3170
|
+
} catch {
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
async function logInfo(context, message) {
|
|
3174
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
|
|
3175
|
+
`);
|
|
3176
|
+
}
|
|
3177
|
+
async function logDebug(context, message) {
|
|
3178
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
3179
|
+
`);
|
|
3180
|
+
}
|
|
3181
|
+
async function logError(context, error) {
|
|
3182
|
+
let errorMsg;
|
|
3183
|
+
if (error instanceof Error) {
|
|
3184
|
+
errorMsg = `${error.message}
|
|
3185
|
+
${error.stack}`;
|
|
3186
|
+
} else if (typeof error === "object" && error !== null) {
|
|
3187
|
+
errorMsg = JSON.stringify(error, null, 2);
|
|
3188
|
+
} else {
|
|
3189
|
+
errorMsg = String(error);
|
|
3190
|
+
}
|
|
3191
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3192
|
+
${errorMsg}
|
|
3193
|
+
|
|
3194
|
+
`);
|
|
3195
|
+
}
|
|
3196
|
+
async function logStep(stepNum, action, details) {
|
|
3197
|
+
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3198
|
+
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3199
|
+
`);
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
// src/ai/mcp-context.ts
|
|
3203
|
+
var import_node_async_hooks = require("async_hooks");
|
|
3204
|
+
var DelegatedGenerationError = class extends Error {
|
|
3205
|
+
tool;
|
|
3206
|
+
step;
|
|
3207
|
+
/** The system instructions the call was about to send. */
|
|
3208
|
+
system;
|
|
3209
|
+
/** The user-side prompt. */
|
|
3210
|
+
prompt;
|
|
3211
|
+
constructor(tool, step, system, prompt2) {
|
|
3212
|
+
super(
|
|
3213
|
+
`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.`
|
|
3214
|
+
);
|
|
3215
|
+
this.name = "DelegatedGenerationError";
|
|
3216
|
+
this.tool = tool;
|
|
3217
|
+
this.step = step;
|
|
3218
|
+
this.system = system;
|
|
3219
|
+
this.prompt = prompt2;
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
var mcpCall = new import_node_async_hooks.AsyncLocalStorage();
|
|
3223
|
+
function currentMcpTool() {
|
|
3224
|
+
return mcpCall.getStore()?.tool ?? null;
|
|
3225
|
+
}
|
|
3226
|
+
function suppliedAnswer(step) {
|
|
3227
|
+
return mcpCall.getStore()?.answers?.[step];
|
|
3228
|
+
}
|
|
3229
|
+
function recordDelegation(e) {
|
|
3230
|
+
const store = mcpCall.getStore();
|
|
3231
|
+
if (store && !store.pending.some((p) => p.step === e.step)) store.pending.push(e);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
2589
3234
|
// src/ai/client.ts
|
|
2590
3235
|
var lastRateLimitInfo = null;
|
|
3236
|
+
function wrap(text, width) {
|
|
3237
|
+
const lines = [];
|
|
3238
|
+
for (const paragraph of text.split("\n")) {
|
|
3239
|
+
let current = "";
|
|
3240
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
3241
|
+
if (current && current.length + word.length + 1 > width) {
|
|
3242
|
+
lines.push(current);
|
|
3243
|
+
current = word;
|
|
3244
|
+
} else {
|
|
3245
|
+
current = current ? `${current} ${word}` : word;
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
lines.push(current);
|
|
3249
|
+
}
|
|
3250
|
+
return lines.filter((l) => l.length > 0);
|
|
3251
|
+
}
|
|
3252
|
+
function diagnose(status, host) {
|
|
3253
|
+
const where = host ?? "the AI service";
|
|
3254
|
+
if (status === void 0) return `Could not reach ${where}. Check your network, then retry.`;
|
|
3255
|
+
if (status === 401 || status === 403) return `${where} rejected the credentials. Run qp login and retry.`;
|
|
3256
|
+
if (status === 413) return `The prompt was too large for ${where}. Shorten the script or the page.`;
|
|
3257
|
+
if (status === 429) return `${where} is rate limiting. Wait for the quota to reset, then retry.`;
|
|
3258
|
+
if (status === 502 || status === 503 || status === 504) {
|
|
3259
|
+
return `${where} could not reach the AI backend (upstream outage). Retry in a few minutes.`;
|
|
3260
|
+
}
|
|
3261
|
+
if (status >= 500) return `${where} failed internally. Retry in a few minutes; if it persists, report this.`;
|
|
3262
|
+
return `${where} rejected the request. See the body above.`;
|
|
3263
|
+
}
|
|
2591
3264
|
function getLastRateLimitInfo() {
|
|
2592
3265
|
return lastRateLimitInfo;
|
|
2593
3266
|
}
|
|
2594
3267
|
function prompt(question) {
|
|
2595
|
-
const rl = (0,
|
|
3268
|
+
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2596
3269
|
return new Promise((resolve9) => {
|
|
2597
3270
|
rl.question(question, (answer) => {
|
|
2598
3271
|
rl.close();
|
|
@@ -2635,15 +3308,33 @@ async function setupUserEmail() {
|
|
|
2635
3308
|
console.log();
|
|
2636
3309
|
return email;
|
|
2637
3310
|
}
|
|
2638
|
-
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
|
|
2639
|
-
const
|
|
3311
|
+
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free", step = "plan") {
|
|
3312
|
+
const mcpTool = currentMcpTool();
|
|
3313
|
+
if (mcpTool) {
|
|
3314
|
+
const answered = suppliedAnswer(step);
|
|
3315
|
+
if (typeof answered === "string") return answered;
|
|
3316
|
+
const refusal = new DelegatedGenerationError(mcpTool, step, systemPrompt, userPrompt);
|
|
3317
|
+
recordDelegation(refusal);
|
|
3318
|
+
throw refusal;
|
|
3319
|
+
}
|
|
3320
|
+
const onRetry = ({ attempt, status, delayMs, host }) => {
|
|
3321
|
+
console.log(
|
|
3322
|
+
import_chalk5.default.dim(
|
|
3323
|
+
`
|
|
3324
|
+
${host} returned ${status}; retrying in ${Math.round(delayMs / 1e3)}s (attempt ${attempt + 1} of 3)`
|
|
3325
|
+
)
|
|
3326
|
+
);
|
|
3327
|
+
};
|
|
3328
|
+
const chatOpts = { onRetry, backoffMs: CLI_BACKOFF_MS };
|
|
3329
|
+
const result = process.env.SPARK_AI_KEY ? await callAI(systemPrompt, userPrompt, tier, chatOpts) : await callAIViaRelay(systemPrompt, userPrompt, userEmail ?? "", tier, chatOpts);
|
|
2640
3330
|
if (!result.success) {
|
|
2641
|
-
await logError2("AI API", new Error(result.error
|
|
3331
|
+
await logError2("AI API", new Error(result.detail ? `${result.error}
|
|
3332
|
+
Body: ${result.detail}` : result.error));
|
|
2642
3333
|
if (result.isRateLimit) {
|
|
2643
3334
|
console.log();
|
|
2644
3335
|
console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2645
3336
|
console.log();
|
|
2646
|
-
console.log(import_chalk5.default.dim(
|
|
3337
|
+
console.log(import_chalk5.default.dim(` ${result.error}`));
|
|
2647
3338
|
console.log(import_chalk5.default.dim(" Options:"));
|
|
2648
3339
|
if (result.retryAfterSeconds) {
|
|
2649
3340
|
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
@@ -2658,17 +3349,31 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
|
|
|
2658
3349
|
}
|
|
2659
3350
|
if (result.isAuthError) {
|
|
2660
3351
|
console.log();
|
|
2661
|
-
console.log(import_chalk5.default.red(
|
|
3352
|
+
console.log(import_chalk5.default.red(` \u26A0 Authentication Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
2662
3353
|
console.log();
|
|
2663
|
-
|
|
3354
|
+
for (const line of wrap(result.error, 76)) {
|
|
3355
|
+
console.log(import_chalk5.default.dim(` ${line}`));
|
|
3356
|
+
}
|
|
3357
|
+
console.log();
|
|
3358
|
+
console.log(import_chalk5.default.dim(` ${diagnose(result.status, result.host)}`));
|
|
2664
3359
|
console.log();
|
|
2665
3360
|
process.exit(1);
|
|
2666
3361
|
}
|
|
2667
3362
|
console.log();
|
|
2668
|
-
console.log(import_chalk5.default.red(
|
|
3363
|
+
console.log(import_chalk5.default.red(` \u26A0 AI Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
2669
3364
|
console.log();
|
|
2670
|
-
const
|
|
2671
|
-
|
|
3365
|
+
for (const line of wrap(result.error, 76)) {
|
|
3366
|
+
console.log(import_chalk5.default.dim(` ${line}`));
|
|
3367
|
+
}
|
|
3368
|
+
if (result.detail && !result.error.includes(result.detail)) {
|
|
3369
|
+
console.log();
|
|
3370
|
+
for (const line of wrap(result.detail, 76).slice(0, 6)) {
|
|
3371
|
+
console.log(import_chalk5.default.dim(` ${line}`));
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
console.log();
|
|
3375
|
+
console.log(import_chalk5.default.dim(` ${diagnose(result.status, result.host)}`));
|
|
3376
|
+
console.log(import_chalk5.default.dim(` Full response body: ${logFilePath}`));
|
|
2672
3377
|
console.log();
|
|
2673
3378
|
process.exit(1);
|
|
2674
3379
|
}
|
|
@@ -2676,7 +3381,48 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
|
|
|
2676
3381
|
return result.content;
|
|
2677
3382
|
}
|
|
2678
3383
|
|
|
3384
|
+
// src/ai/json-plan.ts
|
|
3385
|
+
init_lib();
|
|
3386
|
+
init_utils();
|
|
3387
|
+
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
3388
|
+
var PLAN_SHAPE = `The JSON should have: url, title, description, steps array.
|
|
3389
|
+
Each step has: id (number), action, caption, and optionally target, value.`;
|
|
3390
|
+
var jsonFixUserPrompt = (content, shape) => `Fix this malformed JSON. Return ONLY valid JSON, no explanation.
|
|
3391
|
+
${shape}
|
|
3392
|
+
|
|
3393
|
+
Malformed content:
|
|
3394
|
+
${content}`;
|
|
3395
|
+
var noopTask2 = () => ({ succeed: () => {
|
|
3396
|
+
}, fail: () => {
|
|
3397
|
+
} });
|
|
3398
|
+
async function parseAIJsonWithRepair(content, opts) {
|
|
3399
|
+
const { logError: logError2, userEmail, tier = "free", shape = PLAN_SHAPE, context = "JSON Fix" } = opts;
|
|
3400
|
+
const direct = parseAIJson(content);
|
|
3401
|
+
if (direct) return direct.value;
|
|
3402
|
+
const makeTask = opts.task ?? noopTask2;
|
|
3403
|
+
const fixTask = makeTask("Fixing malformed JSON");
|
|
3404
|
+
try {
|
|
3405
|
+
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content, shape), logError2, userEmail, tier);
|
|
3406
|
+
const repaired = parseAIJson(fixed);
|
|
3407
|
+
if (!repaired) throw new Error("No usable JSON in fix");
|
|
3408
|
+
fixTask.succeed("JSON fixed by AI");
|
|
3409
|
+
return repaired.value;
|
|
3410
|
+
} catch (err) {
|
|
3411
|
+
await logError2(context, err);
|
|
3412
|
+
fixTask.fail("Could not fix JSON");
|
|
3413
|
+
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
|
|
2679
3417
|
// src/ai/narrate.ts
|
|
3418
|
+
var SCRIPT_ACTIONS = /* @__PURE__ */ new Set(["click", "type", "scroll", "wait", "select", "check", "navigate"]);
|
|
3419
|
+
var NEEDS_TARGET = /* @__PURE__ */ new Set(["click", "type", "select", "check", "navigate"]);
|
|
3420
|
+
function isUsableStep(step) {
|
|
3421
|
+
if (!step?.action || !SCRIPT_ACTIONS.has(step.action)) return false;
|
|
3422
|
+
if (NEEDS_TARGET.has(step.action) && !step.target) return false;
|
|
3423
|
+
if (step.action === "navigate" && !/^https?:\/\//.test(step.target ?? "")) return false;
|
|
3424
|
+
return true;
|
|
3425
|
+
}
|
|
2680
3426
|
async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
|
|
2681
3427
|
const langName = getLanguageName(lang);
|
|
2682
3428
|
const stepList = steps.map((s, i) => {
|
|
@@ -2702,33 +3448,34 @@ Steps to caption:
|
|
|
2702
3448
|
${stepList}
|
|
2703
3449
|
|
|
2704
3450
|
Return exactly ${steps.length} captions as a JSON array.`;
|
|
2705
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
2706
|
-
const
|
|
2707
|
-
if (!
|
|
2708
|
-
const captions =
|
|
3451
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "narration");
|
|
3452
|
+
const parsed = parseAIJson(content);
|
|
3453
|
+
if (!parsed || !Array.isArray(parsed.value)) throw new Error("No JSON array in AI caption response");
|
|
3454
|
+
const captions = parsed.value;
|
|
2709
3455
|
return steps.map((s, i) => ({
|
|
2710
3456
|
...s,
|
|
2711
3457
|
id: i + 1,
|
|
2712
3458
|
caption: toTitleCase(captions[i] ?? `Step ${i + 1}.`)
|
|
2713
3459
|
}));
|
|
2714
3460
|
}
|
|
2715
|
-
async function generatePlanFromScript(captions, url, title, pageElementsText, lang, logError2, userEmail, tier = "free") {
|
|
3461
|
+
async function generatePlanFromScript(captions, url, title, pageElementsText, lang, logError2, userEmail, tier = "free", task2) {
|
|
2716
3462
|
const systemPrompt = `You map narration sentences to web UI actions.
|
|
2717
|
-
Given page elements and a script, return
|
|
3463
|
+
Given page elements and a numbered script, return one action per script line.
|
|
2718
3464
|
|
|
2719
3465
|
RULES:
|
|
2720
|
-
- Return ONLY valid JSON
|
|
2721
|
-
-
|
|
3466
|
+
- Return ONLY valid JSON, no explanation, no captions
|
|
3467
|
+
- Return exactly one entry per script line, in order, with "line" set to that line's number
|
|
2722
3468
|
- Use page elements to pick realistic actions/targets
|
|
2723
|
-
- If a
|
|
3469
|
+
- If a line doesn't match a specific UI action, use { "action": "wait" }
|
|
3470
|
+
- Keep every target and value short
|
|
2724
3471
|
|
|
2725
3472
|
ACTION FORMATS:
|
|
2726
|
-
\u2022 click: { "action": "click", "target": "button text" }
|
|
2727
|
-
\u2022 type: { "action": "type", "target": "input selector", "value": "text
|
|
2728
|
-
\u2022 scroll: { "action": "scroll", "value": "up|down" }
|
|
2729
|
-
\u2022 wait: { "action": "wait" }
|
|
3473
|
+
\u2022 click: { "line": 1, "action": "click", "target": "button text" }
|
|
3474
|
+
\u2022 type: { "line": 2, "action": "type", "target": "input selector", "value": "text to type" }
|
|
3475
|
+
\u2022 scroll: { "line": 3, "action": "scroll", "value": "up|down" }
|
|
3476
|
+
\u2022 wait: { "line": 4, "action": "wait" }
|
|
2730
3477
|
|
|
2731
|
-
OUTPUT:
|
|
3478
|
+
OUTPUT: {"description":"one short sentence","steps":[ ... ]}`;
|
|
2732
3479
|
const scriptLines = captions.map((c, i) => `${i + 1}. "${c}"`).join("\n");
|
|
2733
3480
|
const userMessage = `URL: ${url}
|
|
2734
3481
|
App: ${title}
|
|
@@ -2736,22 +3483,46 @@ App: ${title}
|
|
|
2736
3483
|
Available page elements:
|
|
2737
3484
|
${pageElementsText}
|
|
2738
3485
|
|
|
2739
|
-
Narration script (map each line to a UI action
|
|
3486
|
+
Narration script (map each line to a UI action):
|
|
2740
3487
|
${scriptLines}
|
|
2741
3488
|
|
|
2742
|
-
Return
|
|
2743
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
3489
|
+
Return JSON with a "description" and a "steps" array of exactly ${captions.length} actions.`;
|
|
3490
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "narration");
|
|
3491
|
+
const parsed = await parseAIJsonWithRepair(content, {
|
|
3492
|
+
logError: logError2,
|
|
3493
|
+
task: task2,
|
|
3494
|
+
userEmail,
|
|
3495
|
+
tier,
|
|
3496
|
+
shape: "The JSON should have: description (string), steps (array of {line, action, target, value}).",
|
|
3497
|
+
context: "Script Plan"
|
|
3498
|
+
});
|
|
3499
|
+
const raw = Array.isArray(parsed?.steps) ? parsed.steps : [];
|
|
3500
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
3501
|
+
raw.forEach((step, i) => {
|
|
3502
|
+
if (!step || typeof step !== "object") return;
|
|
3503
|
+
const line = typeof step.line === "number" && step.line >= 1 ? step.line : i + 1;
|
|
3504
|
+
if (!byLine.has(line)) byLine.set(line, step);
|
|
3505
|
+
});
|
|
3506
|
+
const steps = captions.map((caption, i) => {
|
|
3507
|
+
const mapped = byLine.get(i + 1);
|
|
3508
|
+
if (!isUsableStep(mapped)) {
|
|
3509
|
+
return { id: i + 1, action: "wait", caption };
|
|
3510
|
+
}
|
|
3511
|
+
return {
|
|
3512
|
+
id: i + 1,
|
|
3513
|
+
action: mapped.action,
|
|
3514
|
+
caption,
|
|
3515
|
+
...mapped.target ? { target: mapped.target } : {},
|
|
3516
|
+
...mapped.value ? { value: mapped.value } : {}
|
|
3517
|
+
};
|
|
3518
|
+
});
|
|
3519
|
+
return {
|
|
3520
|
+
url,
|
|
3521
|
+
title,
|
|
3522
|
+
description: typeof parsed?.description === "string" ? parsed.description : title,
|
|
3523
|
+
...lang !== void 0 ? { lang } : {},
|
|
3524
|
+
steps
|
|
3525
|
+
};
|
|
2755
3526
|
}
|
|
2756
3527
|
|
|
2757
3528
|
// src/browser/events-to-plan.ts
|
|
@@ -2858,152 +3629,9 @@ function normalizeUrl2(url) {
|
|
|
2858
3629
|
}
|
|
2859
3630
|
|
|
2860
3631
|
// src/capture.ts
|
|
2861
|
-
var
|
|
3632
|
+
var import_node_child_process5 = require("child_process");
|
|
2862
3633
|
var import_node_http3 = require("http");
|
|
2863
|
-
var import_node_readline4 = require("readline");
|
|
2864
3634
|
var import_playwright4 = require("playwright");
|
|
2865
|
-
|
|
2866
|
-
// src/browser/wsl.ts
|
|
2867
|
-
var import_node_child_process3 = require("child_process");
|
|
2868
|
-
var import_node_fs5 = require("fs");
|
|
2869
|
-
var import_node_http2 = require("http");
|
|
2870
|
-
var import_playwright3 = require("playwright");
|
|
2871
|
-
function isWSL() {
|
|
2872
|
-
if (process.platform !== "linux") return false;
|
|
2873
|
-
try {
|
|
2874
|
-
const r = (0, import_node_child_process3.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2875
|
-
return r.includes("microsoft") || r.includes("wsl");
|
|
2876
|
-
} catch {
|
|
2877
|
-
return false;
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
function tryEnableWSLInterop() {
|
|
2881
|
-
return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2882
|
-
}
|
|
2883
|
-
function wslToWindowsPath(wslPath) {
|
|
2884
|
-
if (!wslPath) return wslPath;
|
|
2885
|
-
const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
|
|
2886
|
-
if (!m) return wslPath;
|
|
2887
|
-
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2888
|
-
}
|
|
2889
|
-
function findExe(whereExeQuery, ...hardcoded) {
|
|
2890
|
-
for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
|
|
2891
|
-
try {
|
|
2892
|
-
const line = (0, import_node_child_process3.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2893
|
-
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2894
|
-
} catch {
|
|
2895
|
-
}
|
|
2896
|
-
return null;
|
|
2897
|
-
}
|
|
2898
|
-
var findWindowsChrome = () => findExe(
|
|
2899
|
-
"chrome.exe",
|
|
2900
|
-
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2901
|
-
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
|
2902
|
-
);
|
|
2903
|
-
var findWindowsNode = () => findExe("node.exe");
|
|
2904
|
-
function getWindowsHostIP() {
|
|
2905
|
-
try {
|
|
2906
|
-
const m = (0, import_node_child_process3.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2907
|
-
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2908
|
-
);
|
|
2909
|
-
if (m?.[1]) return m[1];
|
|
2910
|
-
} catch {
|
|
2911
|
-
}
|
|
2912
|
-
try {
|
|
2913
|
-
const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2914
|
-
if (m?.[1]) return m[1];
|
|
2915
|
-
} catch {
|
|
2916
|
-
}
|
|
2917
|
-
return "127.0.0.1";
|
|
2918
|
-
}
|
|
2919
|
-
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2920
|
-
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
2921
|
-
return new Promise((resolve9, reject) => {
|
|
2922
|
-
const proc = (0, import_node_child_process3.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2923
|
-
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2924
|
-
let buf = "";
|
|
2925
|
-
proc.stdout.on("data", (d) => {
|
|
2926
|
-
buf += d.toString();
|
|
2927
|
-
const m = buf.match(/PORT:(\d+)/);
|
|
2928
|
-
if (m) {
|
|
2929
|
-
clearTimeout(timer);
|
|
2930
|
-
resolve9({ proc, port: +m[1] });
|
|
2931
|
-
}
|
|
2932
|
-
});
|
|
2933
|
-
proc.on("error", (e) => {
|
|
2934
|
-
clearTimeout(timer);
|
|
2935
|
-
reject(e);
|
|
2936
|
-
});
|
|
2937
|
-
});
|
|
2938
|
-
}
|
|
2939
|
-
function waitForChrome(host, port) {
|
|
2940
|
-
return new Promise((resolve9, reject) => {
|
|
2941
|
-
const deadline = Date.now() + 2e4;
|
|
2942
|
-
const poll = () => {
|
|
2943
|
-
if (Date.now() > deadline) {
|
|
2944
|
-
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2945
|
-
return;
|
|
2946
|
-
}
|
|
2947
|
-
const req = (0, import_node_http2.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2948
|
-
res.resume();
|
|
2949
|
-
resolve9();
|
|
2950
|
-
});
|
|
2951
|
-
req.on("error", () => setTimeout(poll, 600));
|
|
2952
|
-
req.setTimeout(1e3, () => {
|
|
2953
|
-
req.destroy();
|
|
2954
|
-
setTimeout(poll, 600);
|
|
2955
|
-
});
|
|
2956
|
-
req.end();
|
|
2957
|
-
};
|
|
2958
|
-
poll();
|
|
2959
|
-
});
|
|
2960
|
-
}
|
|
2961
|
-
async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
2962
|
-
const debugPort = options.debugPort ?? 9222;
|
|
2963
|
-
const chromePath = findWindowsChrome();
|
|
2964
|
-
if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
|
|
2965
|
-
const nodePath = findWindowsNode();
|
|
2966
|
-
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2967
|
-
const winUserData = wslToWindowsPath(userDataDir);
|
|
2968
|
-
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2969
|
-
const chromeProc = (0, import_node_child_process3.spawn)(
|
|
2970
|
-
chromePath,
|
|
2971
|
-
[
|
|
2972
|
-
`--remote-debugging-port=${debugPort}`,
|
|
2973
|
-
`--user-data-dir=${winUserData}`,
|
|
2974
|
-
...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
|
|
2975
|
-
"--no-first-run",
|
|
2976
|
-
"--no-default-browser-check",
|
|
2977
|
-
"--disable-background-networking",
|
|
2978
|
-
"--disable-sync",
|
|
2979
|
-
"--disable-features=TranslateUI",
|
|
2980
|
-
options.startMaximized ? "--start-maximized" : "--start-minimized"
|
|
2981
|
-
],
|
|
2982
|
-
{ stdio: "ignore", detached: true }
|
|
2983
|
-
);
|
|
2984
|
-
chromeProc.unref();
|
|
2985
|
-
const windowsHost = getWindowsHostIP();
|
|
2986
|
-
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
2987
|
-
try {
|
|
2988
|
-
await waitForChrome(windowsHost, proxyPort);
|
|
2989
|
-
const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
2990
|
-
return {
|
|
2991
|
-
browser,
|
|
2992
|
-
cleanup: async () => {
|
|
2993
|
-
try {
|
|
2994
|
-
await browser.close();
|
|
2995
|
-
} catch {
|
|
2996
|
-
}
|
|
2997
|
-
relay.kill();
|
|
2998
|
-
}
|
|
2999
|
-
};
|
|
3000
|
-
} catch (err) {
|
|
3001
|
-
relay.kill();
|
|
3002
|
-
throw err;
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
// src/capture.ts
|
|
3007
3635
|
var EVENT_TRACKER_SCRIPT = `
|
|
3008
3636
|
(function () {
|
|
3009
3637
|
if (window.__qpTracking) return;
|
|
@@ -3115,16 +3743,6 @@ var EVENT_TRACKER_SCRIPT = `
|
|
|
3115
3743
|
}, { passive: true, capture: true });
|
|
3116
3744
|
}());
|
|
3117
3745
|
`;
|
|
3118
|
-
function waitForEnter2() {
|
|
3119
|
-
return new Promise((resolve9) => {
|
|
3120
|
-
const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3121
|
-
process.stdout.write(" Press Enter when done...\n");
|
|
3122
|
-
rl.once("line", () => {
|
|
3123
|
-
rl.close();
|
|
3124
|
-
resolve9();
|
|
3125
|
-
});
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
3746
|
async function launchCaptureChrome() {
|
|
3129
3747
|
const debugPort = 9222;
|
|
3130
3748
|
const chromePath = findWindowsChrome();
|
|
@@ -3150,9 +3768,9 @@ async function launchCaptureChrome() {
|
|
|
3150
3768
|
return { browser, relay: probe.proc };
|
|
3151
3769
|
}
|
|
3152
3770
|
probe.proc.kill();
|
|
3153
|
-
const winTemp = (0,
|
|
3771
|
+
const winTemp = (0, import_node_child_process5.execSync)("cmd.exe /c echo %TEMP%", { encoding: "utf-8" }).trim().replace(/\r$/, "");
|
|
3154
3772
|
const userDataDir = `${winTemp}\\qp-capture`;
|
|
3155
|
-
(0,
|
|
3773
|
+
(0, import_node_child_process5.spawn)(
|
|
3156
3774
|
chromePath,
|
|
3157
3775
|
[
|
|
3158
3776
|
`--remote-debugging-port=${debugPort}`,
|
|
@@ -3223,7 +3841,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
3223
3841
|
return [title, parts.join(" | ")];
|
|
3224
3842
|
}).catch(() => ["", ""]);
|
|
3225
3843
|
startTime = Date.now();
|
|
3226
|
-
await
|
|
3844
|
+
await waitForEnter();
|
|
3227
3845
|
const durationMs = Date.now() - startTime;
|
|
3228
3846
|
if (relay) {
|
|
3229
3847
|
try {
|
|
@@ -3235,86 +3853,28 @@ async function captureUserSession(url, _workDir) {
|
|
|
3235
3853
|
await context.close();
|
|
3236
3854
|
await browser.close();
|
|
3237
3855
|
}
|
|
3238
|
-
return { events, durationMs, pageTitle, pageContext };
|
|
3239
|
-
}
|
|
3240
|
-
|
|
3241
|
-
// src/utils/logging.ts
|
|
3242
|
-
var import_promises6 = require("fs/promises");
|
|
3243
|
-
var import_node_path9 = require("path");
|
|
3244
|
-
var LOG_FILE_NAME = "quickpeek.log";
|
|
3245
|
-
var logFileInitialized = false;
|
|
3246
|
-
var logFilePath = LOG_FILE_NAME;
|
|
3247
|
-
async function writeLogEntry(entry) {
|
|
3248
|
-
try {
|
|
3249
|
-
if (!logFileInitialized) {
|
|
3250
|
-
await (0, import_promises6.writeFile)(logFilePath, entry);
|
|
3251
|
-
logFileInitialized = true;
|
|
3252
|
-
} else {
|
|
3253
|
-
await (0, import_promises6.appendFile)(logFilePath, entry);
|
|
3254
|
-
}
|
|
3255
|
-
} catch {
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
async function setLogFilePath(outputDir) {
|
|
3259
|
-
logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
|
|
3260
|
-
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3261
|
-
${"=".repeat(60)}
|
|
3262
|
-
|
|
3263
|
-
`;
|
|
3264
|
-
try {
|
|
3265
|
-
await (0, import_promises6.writeFile)(logFilePath, header2);
|
|
3266
|
-
logFileInitialized = true;
|
|
3267
|
-
} catch {
|
|
3268
|
-
}
|
|
3269
|
-
}
|
|
3270
|
-
async function logInfo(context, message) {
|
|
3271
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
|
|
3272
|
-
`);
|
|
3273
|
-
}
|
|
3274
|
-
async function logDebug(context, message) {
|
|
3275
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
3276
|
-
`);
|
|
3277
|
-
}
|
|
3278
|
-
async function logError(context, error) {
|
|
3279
|
-
let errorMsg;
|
|
3280
|
-
if (error instanceof Error) {
|
|
3281
|
-
errorMsg = `${error.message}
|
|
3282
|
-
${error.stack}`;
|
|
3283
|
-
} else if (typeof error === "object" && error !== null) {
|
|
3284
|
-
errorMsg = JSON.stringify(error, null, 2);
|
|
3285
|
-
} else {
|
|
3286
|
-
errorMsg = String(error);
|
|
3287
|
-
}
|
|
3288
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3289
|
-
${errorMsg}
|
|
3290
|
-
|
|
3291
|
-
`);
|
|
3292
|
-
}
|
|
3293
|
-
async function logStep(stepNum, action, details) {
|
|
3294
|
-
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3295
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3296
|
-
`);
|
|
3856
|
+
return { events, durationMs, pageTitle, pageContext };
|
|
3297
3857
|
}
|
|
3298
3858
|
|
|
3299
3859
|
// src/commands/phases/video-phase.ts
|
|
3300
|
-
var
|
|
3301
|
-
var
|
|
3860
|
+
var import_node_fs13 = require("fs");
|
|
3861
|
+
var import_promises14 = require("fs/promises");
|
|
3302
3862
|
var import_node_path18 = require("path");
|
|
3303
3863
|
init_lib();
|
|
3304
3864
|
|
|
3305
3865
|
// src/record.ts
|
|
3306
|
-
var
|
|
3307
|
-
var
|
|
3866
|
+
var import_node_fs11 = require("fs");
|
|
3867
|
+
var import_promises11 = require("fs/promises");
|
|
3308
3868
|
var import_node_path15 = require("path");
|
|
3309
3869
|
var import_chalk6 = __toESM(require("chalk"));
|
|
3310
3870
|
|
|
3311
3871
|
// src/browser/actions.ts
|
|
3312
|
-
var
|
|
3313
|
-
var
|
|
3872
|
+
var import_node_fs8 = require("fs");
|
|
3873
|
+
var import_promises9 = require("fs/promises");
|
|
3314
3874
|
var import_node_path11 = require("path");
|
|
3315
3875
|
|
|
3316
3876
|
// src/browser/effects.ts
|
|
3317
|
-
var
|
|
3877
|
+
var import_node_fs7 = require("fs");
|
|
3318
3878
|
var import_node_path10 = require("path");
|
|
3319
3879
|
init_lib();
|
|
3320
3880
|
function loadHighlightCSS() {
|
|
@@ -3325,7 +3885,7 @@ function loadHighlightCSS() {
|
|
|
3325
3885
|
// src/browser/ → src/
|
|
3326
3886
|
];
|
|
3327
3887
|
for (const p of candidates) {
|
|
3328
|
-
if ((0,
|
|
3888
|
+
if ((0, import_node_fs7.existsSync)(p)) return (0, import_node_fs7.readFileSync)(p, "utf-8");
|
|
3329
3889
|
}
|
|
3330
3890
|
return "";
|
|
3331
3891
|
}
|
|
@@ -3525,6 +4085,10 @@ async function resolveElement(page, target) {
|
|
|
3525
4085
|
await locator.waitFor({ state: "visible", timeout });
|
|
3526
4086
|
return await locator.elementHandle({ timeout: 1e3 });
|
|
3527
4087
|
}
|
|
4088
|
+
if (target.startsWith("#") && !/^#[A-Za-z_-][\w-]*$/.test(target)) {
|
|
4089
|
+
const attr = `[id="${target.slice(1).replace(/(["\\])/g, "\\$1")}"]`;
|
|
4090
|
+
return await page.waitForSelector(attr, { state: "visible", timeout });
|
|
4091
|
+
}
|
|
3528
4092
|
return await page.waitForSelector(target, { state: "visible", timeout });
|
|
3529
4093
|
} catch {
|
|
3530
4094
|
return null;
|
|
@@ -3649,7 +4213,14 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3649
4213
|
}
|
|
3650
4214
|
} else {
|
|
3651
4215
|
const amount = direction === "up" ? -PAGE_SCROLL_PX : PAGE_SCROLL_PX;
|
|
3652
|
-
await page.evaluate((y) =>
|
|
4216
|
+
const moved = await page.evaluate((y) => {
|
|
4217
|
+
const before = window.scrollY;
|
|
4218
|
+
window.scrollBy(0, y);
|
|
4219
|
+
return window.scrollY - before;
|
|
4220
|
+
}, amount);
|
|
4221
|
+
if (moved === 0) {
|
|
4222
|
+
throw new Error("scroll moved nothing: the page has no scroll headroom at this viewport");
|
|
4223
|
+
}
|
|
3653
4224
|
}
|
|
3654
4225
|
break;
|
|
3655
4226
|
}
|
|
@@ -3667,7 +4238,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3667
4238
|
if (!isPlaceholder) {
|
|
3668
4239
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3669
4240
|
const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
|
|
3670
|
-
if ((0,
|
|
4241
|
+
if ((0, import_node_fs8.existsSync)(filePath)) {
|
|
3671
4242
|
await fileInput.setInputFiles(filePath);
|
|
3672
4243
|
await page.waitForTimeout(2e3);
|
|
3673
4244
|
break;
|
|
@@ -3784,7 +4355,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3784
4355
|
break;
|
|
3785
4356
|
case "image": {
|
|
3786
4357
|
const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
|
|
3787
|
-
if (imagePath && (0,
|
|
4358
|
+
if (imagePath && (0, import_node_fs8.existsSync)(imagePath)) {
|
|
3788
4359
|
const imgStyle = step.target === "stretch" ? `width: ${config.video.width}px; height: ${config.video.height}px; object-fit: cover;` : "max-width: 90%; max-height: 90%;";
|
|
3789
4360
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
3790
4361
|
}
|
|
@@ -3814,7 +4385,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3814
4385
|
await pendingDownload;
|
|
3815
4386
|
pendingDownload = null;
|
|
3816
4387
|
}
|
|
3817
|
-
if (lastDownloadPath && (0,
|
|
4388
|
+
if (lastDownloadPath && (0, import_node_fs8.existsSync)(lastDownloadPath)) {
|
|
3818
4389
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
3819
4390
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
3820
4391
|
}
|
|
@@ -3825,7 +4396,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3825
4396
|
async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
3826
4397
|
const { width, height } = config.video;
|
|
3827
4398
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
3828
|
-
const fileBuffer = await (0,
|
|
4399
|
+
const fileBuffer = await (0, import_promises9.readFile)(filePath);
|
|
3829
4400
|
const base64 = fileBuffer.toString("base64");
|
|
3830
4401
|
const mimeTypes = {
|
|
3831
4402
|
png: "image/png",
|
|
@@ -3854,64 +4425,6 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
|
3854
4425
|
return false;
|
|
3855
4426
|
}
|
|
3856
4427
|
|
|
3857
|
-
// src/browser/launch.ts
|
|
3858
|
-
var import_promises8 = require("fs/promises");
|
|
3859
|
-
var import_playwright5 = require("playwright");
|
|
3860
|
-
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
3861
|
-
const extensionPath = config.browser?.extension;
|
|
3862
|
-
const viewport = { width: config.video.width, height: config.video.height };
|
|
3863
|
-
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
3864
|
-
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
3865
|
-
let context;
|
|
3866
|
-
let browser = null;
|
|
3867
|
-
let chromeSession = null;
|
|
3868
|
-
if (extensionPath) {
|
|
3869
|
-
await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
|
|
3870
|
-
const persistentArgs = [
|
|
3871
|
-
`--disable-extensions-except=${extensionPath}`,
|
|
3872
|
-
`--load-extension=${extensionPath}`,
|
|
3873
|
-
"--window-position=-32000,-32000",
|
|
3874
|
-
"--window-size=1,1"
|
|
3875
|
-
];
|
|
3876
|
-
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
3877
|
-
if (useWindowsChrome) {
|
|
3878
|
-
try {
|
|
3879
|
-
chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...videoOpts });
|
|
3880
|
-
const contexts = chromeSession.browser.contexts();
|
|
3881
|
-
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3882
|
-
} catch {
|
|
3883
|
-
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3884
|
-
headless: false,
|
|
3885
|
-
args: persistentArgs,
|
|
3886
|
-
viewport,
|
|
3887
|
-
...videoOpts,
|
|
3888
|
-
acceptDownloads: true
|
|
3889
|
-
});
|
|
3890
|
-
}
|
|
3891
|
-
} else {
|
|
3892
|
-
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3893
|
-
headless: false,
|
|
3894
|
-
args: persistentArgs,
|
|
3895
|
-
viewport,
|
|
3896
|
-
...videoOpts,
|
|
3897
|
-
acceptDownloads: true
|
|
3898
|
-
});
|
|
3899
|
-
}
|
|
3900
|
-
} else {
|
|
3901
|
-
browser = await import_playwright5.chromium.launch({ headless: true });
|
|
3902
|
-
context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
|
|
3903
|
-
}
|
|
3904
|
-
return { context, browser, chromeSession };
|
|
3905
|
-
}
|
|
3906
|
-
async function cleanupBrowserSession(session) {
|
|
3907
|
-
if (session.chromeSession) {
|
|
3908
|
-
await session.chromeSession.cleanup();
|
|
3909
|
-
} else {
|
|
3910
|
-
await session.context.close();
|
|
3911
|
-
if (session.browser) await session.browser.close();
|
|
3912
|
-
}
|
|
3913
|
-
}
|
|
3914
|
-
|
|
3915
4428
|
// src/record.ts
|
|
3916
4429
|
init_lib();
|
|
3917
4430
|
|
|
@@ -3920,16 +4433,16 @@ var import_node_path13 = require("path");
|
|
|
3920
4433
|
init_utils();
|
|
3921
4434
|
|
|
3922
4435
|
// src/browser/step-validator.ts
|
|
3923
|
-
var
|
|
4436
|
+
var import_node_fs9 = require("fs");
|
|
3924
4437
|
var import_node_path12 = require("path");
|
|
3925
4438
|
init_utils();
|
|
3926
4439
|
function findMediaFile(filename, baseDir) {
|
|
3927
4440
|
const name = (0, import_node_path12.basename)(filename);
|
|
3928
4441
|
const searchDirs = [baseDir, (0, import_node_path12.join)(baseDir, "res"), (0, import_node_path12.join)(baseDir, "assets"), (0, import_node_path12.join)(baseDir, "media")];
|
|
3929
4442
|
for (const dir of searchDirs) {
|
|
3930
|
-
if (!(0,
|
|
4443
|
+
if (!(0, import_node_fs9.existsSync)(dir)) continue;
|
|
3931
4444
|
try {
|
|
3932
|
-
const files = (0,
|
|
4445
|
+
const files = (0, import_node_fs9.readdirSync)(dir);
|
|
3933
4446
|
if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
|
|
3934
4447
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
3935
4448
|
if (match) return (0, import_node_path12.join)(dir, match);
|
|
@@ -4005,7 +4518,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
4005
4518
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
4006
4519
|
}
|
|
4007
4520
|
const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
|
|
4008
|
-
if ((0,
|
|
4521
|
+
if ((0, import_node_fs9.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
4009
4522
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
4010
4523
|
if (foundPath) {
|
|
4011
4524
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4127,7 +4640,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
4127
4640
|
if (!isPlaceholder) {
|
|
4128
4641
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
4129
4642
|
let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
|
|
4130
|
-
if (!(0,
|
|
4643
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
4131
4644
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
4132
4645
|
if (foundPath) {
|
|
4133
4646
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4291,8 +4804,8 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
4291
4804
|
}
|
|
4292
4805
|
|
|
4293
4806
|
// src/browser/meta.ts
|
|
4294
|
-
var
|
|
4295
|
-
var
|
|
4807
|
+
var import_node_fs10 = require("fs");
|
|
4808
|
+
var import_promises10 = require("fs/promises");
|
|
4296
4809
|
var import_node_path14 = require("path");
|
|
4297
4810
|
var RECORDING_FILE = "recording.mp4";
|
|
4298
4811
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
@@ -4301,9 +4814,9 @@ function getRecordingPath(outputDir) {
|
|
|
4301
4814
|
}
|
|
4302
4815
|
async function loadRecordingMeta(outputDir) {
|
|
4303
4816
|
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4304
|
-
if (!(0,
|
|
4817
|
+
if (!(0, import_node_fs10.existsSync)(metaPath)) return null;
|
|
4305
4818
|
try {
|
|
4306
|
-
const content = await (0,
|
|
4819
|
+
const content = await (0, import_promises10.readFile)(metaPath, "utf-8");
|
|
4307
4820
|
return JSON.parse(content);
|
|
4308
4821
|
} catch {
|
|
4309
4822
|
return null;
|
|
@@ -4311,7 +4824,7 @@ async function loadRecordingMeta(outputDir) {
|
|
|
4311
4824
|
}
|
|
4312
4825
|
async function saveRecordingMeta(outputDir, meta) {
|
|
4313
4826
|
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4314
|
-
await (0,
|
|
4827
|
+
await (0, import_promises10.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
4315
4828
|
}
|
|
4316
4829
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
4317
4830
|
if (!existingMeta) {
|
|
@@ -4381,14 +4894,14 @@ function formatStepLabel(step) {
|
|
|
4381
4894
|
}
|
|
4382
4895
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4383
4896
|
const videoDir = (0, import_node_path15.join)(workDir, "video");
|
|
4384
|
-
await (0,
|
|
4897
|
+
await (0, import_promises11.mkdir)(videoDir, { recursive: true });
|
|
4385
4898
|
const getStepDuration = (stepNum) => {
|
|
4386
4899
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4387
4900
|
};
|
|
4388
4901
|
const browserTask = task2("Starting browser recording");
|
|
4389
4902
|
const recordingStartTime = Date.now();
|
|
4390
4903
|
const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
|
|
4391
|
-
const recordVideo = { dir: videoDir, size:
|
|
4904
|
+
const recordVideo = { dir: videoDir, size: recordingViewport(config) };
|
|
4392
4905
|
const authState = findAuthState(plan.url);
|
|
4393
4906
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
4394
4907
|
const page = context.pages()[0] || await context.newPage();
|
|
@@ -4409,6 +4922,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4409
4922
|
let onMediaPage = firstStepIsMedia;
|
|
4410
4923
|
const failures = [];
|
|
4411
4924
|
const actualStepDurations = [];
|
|
4925
|
+
let pageClosedEarly = false;
|
|
4412
4926
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
4413
4927
|
const step = plan.steps[i];
|
|
4414
4928
|
const stepNum = i + 1;
|
|
@@ -4435,6 +4949,12 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4435
4949
|
let targetEl = null;
|
|
4436
4950
|
if (step.target) {
|
|
4437
4951
|
targetEl = await resolveElement(page, step.target);
|
|
4952
|
+
if (!targetEl) {
|
|
4953
|
+
console.log(
|
|
4954
|
+
`
|
|
4955
|
+
\u26A0 target "${step.target}" not found on page \u2014 step ${stepNum} recorded without the action`
|
|
4956
|
+
);
|
|
4957
|
+
}
|
|
4438
4958
|
}
|
|
4439
4959
|
if (step.zoom && step.zoom !== 1 && targetEl && !isExtensionPage) {
|
|
4440
4960
|
const preZoomBox = await targetEl.boundingBox() ?? void 0;
|
|
@@ -4471,11 +4991,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4471
4991
|
}
|
|
4472
4992
|
const stepElapsed = Date.now() - stepStartTime;
|
|
4473
4993
|
const waitTime = Math.max(0, stepDuration - stepElapsed);
|
|
4474
|
-
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
4475
4994
|
const stepDelay = config.video.stepDelay ?? 500;
|
|
4476
4995
|
const isLastStep = i === plan.steps.length - 1;
|
|
4477
|
-
|
|
4478
|
-
await page.waitForTimeout(
|
|
4996
|
+
try {
|
|
4997
|
+
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
4998
|
+
if (!isLastStep && stepDelay > 0) await page.waitForTimeout(stepDelay);
|
|
4999
|
+
} catch (err) {
|
|
5000
|
+
if (page.isClosed()) {
|
|
5001
|
+
console.log(import_chalk6.default.yellow("\n \u26A0 Browser page closed early \u2014 saving the recording captured so far"));
|
|
5002
|
+
pageClosedEarly = true;
|
|
5003
|
+
actualStepDurations.push(Math.max(stepElapsed, stepDuration));
|
|
5004
|
+
break;
|
|
5005
|
+
}
|
|
5006
|
+
throw err;
|
|
4479
5007
|
}
|
|
4480
5008
|
if (!isExtensionPage) {
|
|
4481
5009
|
const shouldHighlight = step.highlight !== void 0 ? step.highlight : config.video.highlight;
|
|
@@ -4493,13 +5021,14 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4493
5021
|
console.log(import_chalk6.default.yellow(` - ${f}`));
|
|
4494
5022
|
}
|
|
4495
5023
|
}
|
|
5024
|
+
while (actualStepDurations.length < plan.steps.length) actualStepDurations.push(0);
|
|
4496
5025
|
const saveTask = task2("Saving video");
|
|
4497
5026
|
const video = page.video();
|
|
4498
5027
|
if (!video) throw new Error("No video recorder available");
|
|
4499
|
-
await page.close();
|
|
5028
|
+
if (!pageClosedEarly) await page.close();
|
|
4500
5029
|
const videoPath = await video.path();
|
|
4501
5030
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4502
|
-
if (!videoPath || !(0,
|
|
5031
|
+
if (!videoPath || !(0, import_node_fs11.existsSync)(videoPath)) {
|
|
4503
5032
|
throw new Error("Video file was not created");
|
|
4504
5033
|
}
|
|
4505
5034
|
saveTask.succeed("Video saved");
|
|
@@ -4508,8 +5037,8 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4508
5037
|
|
|
4509
5038
|
// src/utils/video-pipeline.ts
|
|
4510
5039
|
var import_video_kit6 = require("@spark-apps/video-kit");
|
|
4511
|
-
var
|
|
4512
|
-
var
|
|
5040
|
+
var import_node_fs12 = require("fs");
|
|
5041
|
+
var import_promises12 = require("fs/promises");
|
|
4513
5042
|
var import_node_path16 = require("path");
|
|
4514
5043
|
var import_chalk7 = __toESM(require("chalk"));
|
|
4515
5044
|
init_lib();
|
|
@@ -4560,7 +5089,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4560
5089
|
currentTime += audio.duration;
|
|
4561
5090
|
}
|
|
4562
5091
|
const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
|
|
4563
|
-
await (0,
|
|
5092
|
+
await (0, import_promises12.writeFile)(assPath, ass);
|
|
4564
5093
|
return assPath;
|
|
4565
5094
|
}
|
|
4566
5095
|
function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
@@ -4574,7 +5103,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4574
5103
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
4575
5104
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
4576
5105
|
const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
|
|
4577
|
-
if ((0,
|
|
5106
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4578
5107
|
const partial = parsePartial(step.partial);
|
|
4579
5108
|
overlays.push({
|
|
4580
5109
|
path: mediaPath,
|
|
@@ -4603,13 +5132,13 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
4603
5132
|
process.exit(0);
|
|
4604
5133
|
}
|
|
4605
5134
|
const composeTask = task("Building final video");
|
|
4606
|
-
await (0,
|
|
5135
|
+
await (0, import_promises12.mkdir)(config.output.dir, { recursive: true });
|
|
4607
5136
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
4608
5137
|
const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
4609
5138
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
4610
|
-
const hasVoiceover = audioPath && (0,
|
|
5139
|
+
const hasVoiceover = audioPath && (0, import_node_fs12.existsSync)(audioPath);
|
|
4611
5140
|
const musicPath = config.music?.path ? (0, import_node_path16.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path16.resolve)(config.output.dir, config.music.path) : "";
|
|
4612
|
-
const hasMusic = musicPath && (0,
|
|
5141
|
+
const hasMusic = musicPath && (0, import_node_fs12.existsSync)(musicPath);
|
|
4613
5142
|
try {
|
|
4614
5143
|
await composeVideo({
|
|
4615
5144
|
videoPath,
|
|
@@ -4674,19 +5203,20 @@ var import_chalk9 = __toESM(require("chalk"));
|
|
|
4674
5203
|
init_lib();
|
|
4675
5204
|
|
|
4676
5205
|
// src/plan.ts
|
|
4677
|
-
var
|
|
5206
|
+
var import_promises13 = require("fs/promises");
|
|
4678
5207
|
var import_node_path17 = require("path");
|
|
4679
5208
|
var import_chalk8 = __toESM(require("chalk"));
|
|
4680
|
-
var
|
|
5209
|
+
var import_playwright5 = require("playwright");
|
|
4681
5210
|
init_lib();
|
|
5211
|
+
init_json_repair();
|
|
4682
5212
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
4683
5213
|
const browserTask = task2("Launching browser with extension");
|
|
4684
5214
|
const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
|
|
4685
|
-
await (0,
|
|
5215
|
+
await (0, import_promises13.mkdir)(userDataDir, { recursive: true });
|
|
4686
5216
|
let context;
|
|
4687
5217
|
let chromeSession = null;
|
|
4688
5218
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
4689
|
-
const launchPersistent = () =>
|
|
5219
|
+
const launchPersistent = () => import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
4690
5220
|
headless: false,
|
|
4691
5221
|
args: [
|
|
4692
5222
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -4767,7 +5297,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4767
5297
|
else if (el.type === "select" || el.type === "select-one") action = "select";
|
|
4768
5298
|
else if (el.type === "checkbox") action = "check";
|
|
4769
5299
|
else if (["text", "email", "password", "number", "search", "url", "tel"].includes(el.type)) action = "type";
|
|
4770
|
-
const selector = el.id ?
|
|
5300
|
+
const selector = el.id ? idSelector(el.id) : el.name ? `[name="${el.name}"]` : `text="${el.text}"`;
|
|
4771
5301
|
return {
|
|
4772
5302
|
type: el.type === "button" || el.type === "link" ? el.type : "input",
|
|
4773
5303
|
text: el.text,
|
|
@@ -4779,43 +5309,19 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4779
5309
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
4780
5310
|
return { title, elements };
|
|
4781
5311
|
}
|
|
4782
|
-
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
4783
|
-
var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4784
|
-
The JSON should have: url, title, description, steps array.
|
|
4785
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
4786
|
-
|
|
4787
|
-
Malformed content:
|
|
4788
|
-
${content}`;
|
|
4789
5312
|
async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
|
|
4790
|
-
|
|
4791
|
-
return JSON.parse(content);
|
|
4792
|
-
} catch {
|
|
4793
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
4794
|
-
try {
|
|
4795
|
-
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
|
|
4796
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4797
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4798
|
-
const plan = JSON.parse(jsonMatch[0]);
|
|
4799
|
-
fixTask.succeed("JSON fixed by AI");
|
|
4800
|
-
return plan;
|
|
4801
|
-
} catch (err) {
|
|
4802
|
-
await logError2("JSON Fix", err);
|
|
4803
|
-
fixTask.fail("Could not fix JSON");
|
|
4804
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4805
|
-
}
|
|
4806
|
-
}
|
|
5313
|
+
return parseAIJsonWithRepair(content, { logError: logError2, task: task2, userEmail, tier });
|
|
4807
5314
|
}
|
|
4808
|
-
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free") {
|
|
4809
|
-
const { title, elements } = extensionPath && workDir ? await crawlExtension(url, extensionPath, workDir, task2) : await crawlPage(url, task2, findAuthState(url));
|
|
5315
|
+
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
|
|
5316
|
+
const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
|
|
4810
5317
|
const aiTask = task2("Generating plan with AI");
|
|
4811
5318
|
const langName = getLanguageName(lang);
|
|
4812
5319
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
4813
|
-
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps);
|
|
5320
|
+
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
|
|
4814
5321
|
try {
|
|
4815
|
-
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
4816
|
-
const
|
|
4817
|
-
if (!
|
|
4818
|
-
const plan = await parsePlanJson(jsonMatch[0], task2, logError2, userEmail, tier);
|
|
5322
|
+
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier, "plan");
|
|
5323
|
+
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
5324
|
+
if (!Array.isArray(plan.steps)) throw new Error("AI response has no steps array");
|
|
4819
5325
|
if (lang !== void 0) plan.lang = lang;
|
|
4820
5326
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
4821
5327
|
...s,
|
|
@@ -4832,14 +5338,14 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4832
5338
|
}
|
|
4833
5339
|
}
|
|
4834
5340
|
async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
|
|
4835
|
-
const content = await (0,
|
|
5341
|
+
const content = await (0, import_promises13.readFile)(planPath, "utf-8");
|
|
4836
5342
|
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
4837
5343
|
if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
|
|
4838
5344
|
throw new Error("Invalid plan: missing url or steps");
|
|
4839
5345
|
}
|
|
4840
5346
|
const renumberTask = task2("Validating plan structure");
|
|
4841
5347
|
const originalCount = plan.steps.length;
|
|
4842
|
-
const
|
|
5348
|
+
const INVALID_TARGETS2 = [/\.png$/i, /\.jpg$/i, /\.gif$/i, /\.webp$/i, /\.svg$/i, /sprite_sheet/i, /downloaded/i];
|
|
4843
5349
|
const isValidStep = (s) => {
|
|
4844
5350
|
if (!s?.action) return false;
|
|
4845
5351
|
if (s.action === "navigate") {
|
|
@@ -4848,7 +5354,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
4848
5354
|
}
|
|
4849
5355
|
if (["click", "type", "select", "check", "upload", "drag"].includes(s.action)) {
|
|
4850
5356
|
if (!s.target) return false;
|
|
4851
|
-
if (
|
|
5357
|
+
if (INVALID_TARGETS2.some((pattern) => pattern.test(s.target))) return false;
|
|
4852
5358
|
}
|
|
4853
5359
|
if (s.action === "upload") {
|
|
4854
5360
|
if (!s.target?.includes("file") && !s.target?.includes("input")) return false;
|
|
@@ -4865,7 +5371,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
4865
5371
|
renumberTask.fail("No valid steps");
|
|
4866
5372
|
throw new Error("Plan has no valid steps");
|
|
4867
5373
|
}
|
|
4868
|
-
await (0,
|
|
5374
|
+
await (0, import_promises13.writeFile)(planPath, JSON.stringify(plan, null, 2));
|
|
4869
5375
|
const removed = originalCount - plan.steps.length;
|
|
4870
5376
|
renumberTask.succeed(
|
|
4871
5377
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -4980,6 +5486,21 @@ function getOutputDirFromUrl(url) {
|
|
|
4980
5486
|
|
|
4981
5487
|
// src/commands/phases/video-phase.ts
|
|
4982
5488
|
var INDENT4 = " ";
|
|
5489
|
+
function resolveOutro(config, userTier) {
|
|
5490
|
+
if (userTier !== "free") {
|
|
5491
|
+
const custom = config.video.outro?.trim();
|
|
5492
|
+
if (!custom) return void 0;
|
|
5493
|
+
const path = (0, import_node_path18.resolve)(custom);
|
|
5494
|
+
if ((0, import_node_fs13.existsSync)(path)) return path;
|
|
5495
|
+
info(`${INDENT4}\u26A0 outro not found: ${path}`);
|
|
5496
|
+
return void 0;
|
|
5497
|
+
}
|
|
5498
|
+
const scriptDir = getScriptDir();
|
|
5499
|
+
const bundled = (0, import_node_path18.join)(scriptDir, "assets", "qp.wm");
|
|
5500
|
+
if ((0, import_node_fs13.existsSync)(bundled)) return bundled;
|
|
5501
|
+
const fallback = (0, import_node_path18.join)(scriptDir, "..", "dist", "assets", "qp.wm");
|
|
5502
|
+
return (0, import_node_fs13.existsSync)(fallback) ? fallback : void 0;
|
|
5503
|
+
}
|
|
4983
5504
|
async function runVideoPhase(params) {
|
|
4984
5505
|
let { stepAudio } = params;
|
|
4985
5506
|
const { plan, config, voiceoverPath, workDir, captions, userTier } = params;
|
|
@@ -4998,7 +5519,7 @@ async function runVideoPhase(params) {
|
|
|
4998
5519
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
4999
5520
|
} else {
|
|
5000
5521
|
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5001
|
-
if ((0,
|
|
5522
|
+
if ((0, import_node_fs13.existsSync)(mediaPath)) {
|
|
5002
5523
|
try {
|
|
5003
5524
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
5004
5525
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -5010,7 +5531,7 @@ async function runVideoPhase(params) {
|
|
|
5010
5531
|
}
|
|
5011
5532
|
} else {
|
|
5012
5533
|
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5013
|
-
if ((0,
|
|
5534
|
+
if ((0, import_node_fs13.existsSync)(mediaPath)) {
|
|
5014
5535
|
try {
|
|
5015
5536
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
5016
5537
|
} catch {
|
|
@@ -5023,7 +5544,7 @@ async function runVideoPhase(params) {
|
|
|
5023
5544
|
})
|
|
5024
5545
|
);
|
|
5025
5546
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
5026
|
-
const hasExistingRecording = existingRecordingMeta && (0,
|
|
5547
|
+
const hasExistingRecording = existingRecordingMeta && (0, import_node_fs13.existsSync)(recordingPath);
|
|
5027
5548
|
let videoPath = recordingPath;
|
|
5028
5549
|
const firstStep = plan.steps[0];
|
|
5029
5550
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -5032,7 +5553,7 @@ async function runVideoPhase(params) {
|
|
|
5032
5553
|
header("Record");
|
|
5033
5554
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
5034
5555
|
stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
|
|
5035
|
-
await (0,
|
|
5556
|
+
await (0, import_promises14.copyFile)(result.videoPath, recordingPath);
|
|
5036
5557
|
const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
|
|
5037
5558
|
meta.pageLoadTime = result.pageLoadTime;
|
|
5038
5559
|
await saveRecordingMeta(config.output.dir, meta);
|
|
@@ -5084,7 +5605,7 @@ async function runVideoPhase(params) {
|
|
|
5084
5605
|
const transitionTask = task("Applying transitions");
|
|
5085
5606
|
try {
|
|
5086
5607
|
const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
|
|
5087
|
-
const videoDuration =
|
|
5608
|
+
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
5088
5609
|
await applyTransitions({
|
|
5089
5610
|
inputVideo: videoPath,
|
|
5090
5611
|
outputVideo: transitionedPath,
|
|
@@ -5102,30 +5623,24 @@ async function runVideoPhase(params) {
|
|
|
5102
5623
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5103
5624
|
let videoToCompose = videoPath;
|
|
5104
5625
|
let mainContentDuration;
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
});
|
|
5124
|
-
videoToCompose = combinedPath;
|
|
5125
|
-
} catch (err) {
|
|
5126
|
-
await logError("Concat Outro", err);
|
|
5127
|
-
mainContentDuration = void 0;
|
|
5128
|
-
}
|
|
5626
|
+
const outroPath = resolveOutro(config, userTier);
|
|
5627
|
+
if (outroPath) {
|
|
5628
|
+
try {
|
|
5629
|
+
const mainDuration = await getMediaDuration(videoPath);
|
|
5630
|
+
mainContentDuration = mainDuration / 1e3 - trimStart;
|
|
5631
|
+
const combinedPath = (0, import_node_path18.join)(workDir, "combined-with-outro.mp4");
|
|
5632
|
+
await concatVideoWithOutro({
|
|
5633
|
+
inputPath: videoPath,
|
|
5634
|
+
outroPath,
|
|
5635
|
+
outputPath: combinedPath,
|
|
5636
|
+
width: config.video.width,
|
|
5637
|
+
height: config.video.height,
|
|
5638
|
+
preset: config.video.preset
|
|
5639
|
+
});
|
|
5640
|
+
videoToCompose = combinedPath;
|
|
5641
|
+
} catch (err) {
|
|
5642
|
+
await logError("Concat Outro", err);
|
|
5643
|
+
mainContentDuration = void 0;
|
|
5129
5644
|
}
|
|
5130
5645
|
}
|
|
5131
5646
|
let finalPath = await composeAndFinish(
|
|
@@ -5159,7 +5674,7 @@ async function runVideoPhase(params) {
|
|
|
5159
5674
|
}
|
|
5160
5675
|
|
|
5161
5676
|
// src/commands/phases/voiceover-phase.ts
|
|
5162
|
-
var
|
|
5677
|
+
var import_node_fs14 = require("fs");
|
|
5163
5678
|
var import_node_path19 = require("path");
|
|
5164
5679
|
var import_chalk10 = __toESM(require("chalk"));
|
|
5165
5680
|
init_lib();
|
|
@@ -5169,9 +5684,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5169
5684
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
5170
5685
|
for (const file of filesToDelete) {
|
|
5171
5686
|
const filePath = (0, import_node_path19.join)(config.output.dir, file);
|
|
5172
|
-
if ((0,
|
|
5173
|
-
const { unlink:
|
|
5174
|
-
await
|
|
5687
|
+
if ((0, import_node_fs14.existsSync)(filePath)) {
|
|
5688
|
+
const { unlink: unlink4 } = await import("fs/promises");
|
|
5689
|
+
await unlink4(filePath).catch(() => {
|
|
5175
5690
|
});
|
|
5176
5691
|
}
|
|
5177
5692
|
}
|
|
@@ -5180,7 +5695,12 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5180
5695
|
const voice = resolveVoice(config);
|
|
5181
5696
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5182
5697
|
const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
|
|
5183
|
-
const
|
|
5698
|
+
const voiceoverOnDisk = existingMeta && (0, import_node_fs14.existsSync)(voiceoverFile);
|
|
5699
|
+
const staleContainer = voiceoverOnDisk ? !await isHonestWav(voiceoverFile) : false;
|
|
5700
|
+
if (staleContainer) {
|
|
5701
|
+
info("Existing voiceover has a stale container, regenerating");
|
|
5702
|
+
}
|
|
5703
|
+
const hasExistingVoiceover = voiceoverOnDisk && !staleContainer;
|
|
5184
5704
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5185
5705
|
let voiceoverPath;
|
|
5186
5706
|
let stepAudio;
|
|
@@ -5270,7 +5790,8 @@ async function captureCommand(url, lang) {
|
|
|
5270
5790
|
lang,
|
|
5271
5791
|
logError,
|
|
5272
5792
|
userData.email,
|
|
5273
|
-
captureResult.pageContext
|
|
5793
|
+
captureResult.pageContext,
|
|
5794
|
+
userData.tier ?? "free"
|
|
5274
5795
|
);
|
|
5275
5796
|
narrateTask.succeed(`${steps.length} captions generated`);
|
|
5276
5797
|
} catch (err) {
|
|
@@ -5314,16 +5835,172 @@ async function captureCommand(url, lang) {
|
|
|
5314
5835
|
console.log(import_chalk11.default.cyan(` ${finalPath}`));
|
|
5315
5836
|
}
|
|
5316
5837
|
|
|
5317
|
-
// src/commands/
|
|
5318
|
-
var
|
|
5319
|
-
var import_promises14 = require("fs/promises");
|
|
5838
|
+
// src/commands/edit-cmd.ts
|
|
5839
|
+
var import_node_fs15 = require("fs");
|
|
5320
5840
|
var import_node_path21 = require("path");
|
|
5321
5841
|
var import_chalk12 = __toESM(require("chalk"));
|
|
5842
|
+
|
|
5843
|
+
// src/lib/plan-validation.ts
|
|
5844
|
+
var INVALID_TARGETS = [
|
|
5845
|
+
/\.png$/i,
|
|
5846
|
+
/\.jpg$/i,
|
|
5847
|
+
/\.gif$/i,
|
|
5848
|
+
/\.webp$/i,
|
|
5849
|
+
/\.svg$/i,
|
|
5850
|
+
/sprite_sheet/i,
|
|
5851
|
+
/downloaded/i
|
|
5852
|
+
];
|
|
5853
|
+
var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
|
|
5854
|
+
function isFilePath(value) {
|
|
5855
|
+
if (!value) return false;
|
|
5856
|
+
return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
|
|
5857
|
+
}
|
|
5858
|
+
function isInvalidTarget(target) {
|
|
5859
|
+
return INVALID_TARGETS.some((pattern) => pattern.test(target));
|
|
5860
|
+
}
|
|
5861
|
+
function validateStep(step) {
|
|
5862
|
+
if (!step?.action) {
|
|
5863
|
+
return "Missing action";
|
|
5864
|
+
}
|
|
5865
|
+
if (step.action === "navigate") {
|
|
5866
|
+
if (!step.target) return "Navigate requires a URL target";
|
|
5867
|
+
if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
|
|
5868
|
+
return "Navigate target must be a valid URL";
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
if (step.action === "media") {
|
|
5872
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
5873
|
+
return "Media step requires a file (image or video)";
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
if (step.action === "upload") {
|
|
5877
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
5878
|
+
return "Upload step requires a file";
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
|
|
5882
|
+
if (!step.target) return `${step.action} requires a target`;
|
|
5883
|
+
if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
|
|
5884
|
+
}
|
|
5885
|
+
return null;
|
|
5886
|
+
}
|
|
5887
|
+
|
|
5888
|
+
// src/lib/plan-file.ts
|
|
5889
|
+
var MAX_HANDOFF_URL_CHARS = 32e3;
|
|
5890
|
+
function serializePlan(plan) {
|
|
5891
|
+
return `${JSON.stringify(plan, null, 2)}
|
|
5892
|
+
`;
|
|
5893
|
+
}
|
|
5894
|
+
function parsePlanText(text) {
|
|
5895
|
+
let raw;
|
|
5896
|
+
try {
|
|
5897
|
+
raw = JSON.parse(text);
|
|
5898
|
+
} catch (e) {
|
|
5899
|
+
return { ok: false, error: `Not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
5900
|
+
}
|
|
5901
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
5902
|
+
return { ok: false, error: "A plan must be a JSON object." };
|
|
5903
|
+
}
|
|
5904
|
+
const candidate = raw;
|
|
5905
|
+
if (typeof candidate.url !== "string" || !candidate.url.trim()) {
|
|
5906
|
+
return { ok: false, error: "A plan needs a `url` to record." };
|
|
5907
|
+
}
|
|
5908
|
+
if (!Array.isArray(candidate.steps) || candidate.steps.length === 0) {
|
|
5909
|
+
return { ok: false, error: "A plan needs at least one step." };
|
|
5910
|
+
}
|
|
5911
|
+
const steps = [];
|
|
5912
|
+
for (const [index, step] of candidate.steps.entries()) {
|
|
5913
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
5914
|
+
return { ok: false, error: `Step ${index + 1} is not an object.` };
|
|
5915
|
+
}
|
|
5916
|
+
const typed = step;
|
|
5917
|
+
if (typeof typed.action !== "string" || !typed.action.trim()) {
|
|
5918
|
+
return { ok: false, error: `Step ${index + 1} has no action.` };
|
|
5919
|
+
}
|
|
5920
|
+
const failure = validateStep(typed);
|
|
5921
|
+
if (failure) return { ok: false, error: `Step ${index + 1}: ${failure}` };
|
|
5922
|
+
steps.push({ ...typed, id: typed.id ?? index + 1 });
|
|
5923
|
+
}
|
|
5924
|
+
return {
|
|
5925
|
+
ok: true,
|
|
5926
|
+
plan: {
|
|
5927
|
+
url: candidate.url,
|
|
5928
|
+
title: candidate.title ?? "",
|
|
5929
|
+
description: candidate.description ?? "",
|
|
5930
|
+
...candidate.lang ? { lang: candidate.lang } : {},
|
|
5931
|
+
steps
|
|
5932
|
+
}
|
|
5933
|
+
};
|
|
5934
|
+
}
|
|
5935
|
+
function toBase64Url(text) {
|
|
5936
|
+
const bytes = new TextEncoder().encode(text);
|
|
5937
|
+
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
|
|
5938
|
+
const base64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(text, "utf8").toString("base64");
|
|
5939
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
5940
|
+
}
|
|
5941
|
+
function encodePlan(plan) {
|
|
5942
|
+
return toBase64Url(serializePlan(plan));
|
|
5943
|
+
}
|
|
5944
|
+
function planEditorUrl(baseUrl, plan) {
|
|
5945
|
+
const url = `${baseUrl.replace(/\/$/, "")}/edit#plan=${encodePlan(plan)}`;
|
|
5946
|
+
return url.length <= MAX_HANDOFF_URL_CHARS ? url : null;
|
|
5947
|
+
}
|
|
5948
|
+
|
|
5949
|
+
// src/commands/edit-cmd.ts
|
|
5950
|
+
var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
5951
|
+
function findPlanFile(dir, lang) {
|
|
5952
|
+
if (lang) {
|
|
5953
|
+
const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
|
|
5954
|
+
return (0, import_node_fs15.existsSync)(exact) ? exact : null;
|
|
5955
|
+
}
|
|
5956
|
+
if (!(0, import_node_fs15.existsSync)(dir)) return null;
|
|
5957
|
+
const plans = (0, import_node_fs15.readdirSync)(dir).filter((f) => PLAN_FILE.test(f));
|
|
5958
|
+
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
5959
|
+
return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
|
|
5960
|
+
}
|
|
5961
|
+
async function editCommand(url, lang) {
|
|
5962
|
+
console.log();
|
|
5963
|
+
showLogo();
|
|
5964
|
+
const dir = getOutputDirFromUrl(url);
|
|
5965
|
+
const planPath = findPlanFile(dir, lang);
|
|
5966
|
+
if (!planPath) {
|
|
5967
|
+
console.log(import_chalk12.default.red(` No plan found in ${dir}`));
|
|
5968
|
+
console.log(import_chalk12.default.dim(` Record the demo first: `) + import_chalk12.default.cyan(`qp ${url}`));
|
|
5969
|
+
console.log();
|
|
5970
|
+
return;
|
|
5971
|
+
}
|
|
5972
|
+
const parsed = parsePlanText((0, import_node_fs15.readFileSync)(planPath, "utf8"));
|
|
5973
|
+
if (!parsed.ok) {
|
|
5974
|
+
console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
|
|
5975
|
+
console.log();
|
|
5976
|
+
return;
|
|
5977
|
+
}
|
|
5978
|
+
const base = editorBaseUrl();
|
|
5979
|
+
const editorUrl = planEditorUrl(base, parsed.plan);
|
|
5980
|
+
if (!editorUrl) {
|
|
5981
|
+
console.log(import_chalk12.default.yellow(" This plan is too long to carry in a link."));
|
|
5982
|
+
console.log(import_chalk12.default.dim(` Open ${base}/edit and load `) + import_chalk12.default.cyan(formatClickablePath(planPath)));
|
|
5983
|
+
console.log();
|
|
5984
|
+
await openInBrowser(`${base}/edit`);
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5987
|
+
await openInBrowser(editorUrl);
|
|
5988
|
+
console.log(import_chalk12.default.green(` \u2713 Opened ${parsed.plan.steps.length} steps in the editor`));
|
|
5989
|
+
console.log(import_chalk12.default.dim(` ${formatClickablePath(planPath)}`));
|
|
5990
|
+
console.log(import_chalk12.default.dim(" Save from the editor over this file, then record again."));
|
|
5991
|
+
console.log();
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5994
|
+
// src/commands/init-cmd.ts
|
|
5995
|
+
var import_node_fs16 = require("fs");
|
|
5996
|
+
var import_promises15 = require("fs/promises");
|
|
5997
|
+
var import_node_path22 = require("path");
|
|
5998
|
+
var import_chalk13 = __toESM(require("chalk"));
|
|
5322
5999
|
init_lib();
|
|
5323
6000
|
async function initCommand() {
|
|
5324
|
-
const configPath = (0,
|
|
5325
|
-
if ((0,
|
|
5326
|
-
console.log(
|
|
6001
|
+
const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
|
|
6002
|
+
if ((0, import_node_fs16.existsSync)(configPath)) {
|
|
6003
|
+
console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
|
|
5327
6004
|
return;
|
|
5328
6005
|
}
|
|
5329
6006
|
const defaultConfig = {
|
|
@@ -5346,32 +6023,32 @@ async function initCommand() {
|
|
|
5346
6023
|
volume: 0.3
|
|
5347
6024
|
}
|
|
5348
6025
|
};
|
|
5349
|
-
await (0,
|
|
6026
|
+
await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
5350
6027
|
`);
|
|
5351
|
-
console.log(
|
|
5352
|
-
console.log(
|
|
6028
|
+
console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
6029
|
+
console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
|
|
5353
6030
|
}
|
|
5354
6031
|
async function configCommand(key, value) {
|
|
5355
|
-
const configPath = (0,
|
|
6032
|
+
const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
|
|
5356
6033
|
if (!key) {
|
|
5357
|
-
if (!(0,
|
|
5358
|
-
console.log(
|
|
6034
|
+
if (!(0, import_node_fs16.existsSync)(configPath)) {
|
|
6035
|
+
console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
5359
6036
|
return;
|
|
5360
6037
|
}
|
|
5361
|
-
const content = await (0,
|
|
5362
|
-
console.log(
|
|
6038
|
+
const content = await (0, import_promises15.readFile)(configPath, "utf-8");
|
|
6039
|
+
console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
|
|
5363
6040
|
console.log(content);
|
|
5364
6041
|
return;
|
|
5365
6042
|
}
|
|
5366
6043
|
if (!value) {
|
|
5367
|
-
console.log(
|
|
5368
|
-
console.log(
|
|
6044
|
+
console.log(import_chalk13.default.red(` Missing value for ${key}`));
|
|
6045
|
+
console.log(import_chalk13.default.dim(` Usage: qp config ${key} <value>`));
|
|
5369
6046
|
return;
|
|
5370
6047
|
}
|
|
5371
6048
|
let config = {};
|
|
5372
|
-
if ((0,
|
|
6049
|
+
if ((0, import_node_fs16.existsSync)(configPath)) {
|
|
5373
6050
|
try {
|
|
5374
|
-
config = JSON.parse(await (0,
|
|
6051
|
+
config = JSON.parse(await (0, import_promises15.readFile)(configPath, "utf-8"));
|
|
5375
6052
|
} catch {
|
|
5376
6053
|
}
|
|
5377
6054
|
}
|
|
@@ -5393,42 +6070,42 @@ async function configCommand(key, value) {
|
|
|
5393
6070
|
}
|
|
5394
6071
|
obj[keys[keys.length - 1]] = parsedValue;
|
|
5395
6072
|
}
|
|
5396
|
-
await (0,
|
|
6073
|
+
await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
|
|
5397
6074
|
`);
|
|
5398
|
-
console.log(
|
|
6075
|
+
console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
|
|
5399
6076
|
}
|
|
5400
6077
|
|
|
5401
6078
|
// src/commands/login-cmd.ts
|
|
5402
|
-
var
|
|
6079
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
5403
6080
|
init_lib();
|
|
5404
6081
|
async function loginCommand() {
|
|
5405
6082
|
console.log();
|
|
5406
6083
|
showLogo();
|
|
5407
6084
|
const existing = await loadUserData();
|
|
5408
6085
|
if (existing.email && existing.verified) {
|
|
5409
|
-
console.log(
|
|
5410
|
-
console.log(
|
|
6086
|
+
console.log(import_chalk14.default.dim(` Already signed in as ${existing.email}`));
|
|
6087
|
+
console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp logout") + import_chalk14.default.dim(" first to switch accounts."));
|
|
5411
6088
|
console.log();
|
|
5412
6089
|
try {
|
|
5413
6090
|
const status = await getStatusCached(existing.email);
|
|
5414
6091
|
const tier2 = await getTierByEmail(existing.email);
|
|
5415
|
-
const tierLabel2 = tier2 === "pro" ?
|
|
6092
|
+
const tierLabel2 = tier2 === "pro" ? import_chalk14.default.green("Pro") : "Free";
|
|
5416
6093
|
console.log(` ${tierLabel2} plan`);
|
|
5417
6094
|
for (const line of formatUsageLines(status, tier2)) {
|
|
5418
|
-
console.log(
|
|
6095
|
+
console.log(import_chalk14.default.dim(` ${line}`));
|
|
5419
6096
|
}
|
|
5420
6097
|
} catch {
|
|
5421
6098
|
}
|
|
5422
6099
|
console.log();
|
|
5423
6100
|
return;
|
|
5424
6101
|
}
|
|
5425
|
-
console.log(
|
|
5426
|
-
console.log(
|
|
6102
|
+
console.log(import_chalk14.default.white(" Sign in with your Google account to track usage"));
|
|
6103
|
+
console.log(import_chalk14.default.white(" and unlock Pro features."));
|
|
5427
6104
|
const email = await browserLogin();
|
|
5428
6105
|
if (!email) {
|
|
5429
6106
|
console.log();
|
|
5430
|
-
console.log(
|
|
5431
|
-
console.log(
|
|
6107
|
+
console.log(import_chalk14.default.yellow(" Sign-in cancelled or timed out."));
|
|
6108
|
+
console.log(import_chalk14.default.dim(" Run ") + import_chalk14.default.cyan("qp login") + import_chalk14.default.dim(" to try again."));
|
|
5432
6109
|
console.log();
|
|
5433
6110
|
return;
|
|
5434
6111
|
}
|
|
@@ -5439,8 +6116,8 @@ async function loginCommand() {
|
|
|
5439
6116
|
}
|
|
5440
6117
|
await saveUserData({ email, verified: true, tier });
|
|
5441
6118
|
console.log();
|
|
5442
|
-
const tierLabel = tier === "pro" ?
|
|
5443
|
-
console.log(
|
|
6119
|
+
const tierLabel = tier === "pro" ? import_chalk14.default.green("Pro") : "Free";
|
|
6120
|
+
console.log(import_chalk14.default.green(` \u2713 Signed in as ${email}`));
|
|
5444
6121
|
console.log(` ${tierLabel} plan`);
|
|
5445
6122
|
console.log();
|
|
5446
6123
|
}
|
|
@@ -5448,46 +6125,46 @@ async function logoutCommand() {
|
|
|
5448
6125
|
console.log();
|
|
5449
6126
|
const userData = await loadUserData();
|
|
5450
6127
|
if (!userData.email) {
|
|
5451
|
-
console.log(
|
|
6128
|
+
console.log(import_chalk14.default.dim(" Not signed in."));
|
|
5452
6129
|
console.log();
|
|
5453
6130
|
return;
|
|
5454
6131
|
}
|
|
5455
6132
|
const email = userData.email;
|
|
5456
6133
|
await saveUserData({ email: "", verified: false, tier: "free" });
|
|
5457
|
-
console.log(
|
|
6134
|
+
console.log(import_chalk14.default.green(` \u2713 Signed out (${email})`));
|
|
5458
6135
|
console.log();
|
|
5459
6136
|
}
|
|
5460
6137
|
|
|
5461
6138
|
// src/commands/record-cmd.ts
|
|
5462
|
-
var
|
|
5463
|
-
var
|
|
5464
|
-
var
|
|
5465
|
-
var
|
|
6139
|
+
var import_node_fs19 = require("fs");
|
|
6140
|
+
var import_promises18 = require("fs/promises");
|
|
6141
|
+
var import_node_path25 = require("path");
|
|
6142
|
+
var import_chalk19 = __toESM(require("chalk"));
|
|
5466
6143
|
init_lib();
|
|
5467
6144
|
|
|
5468
6145
|
// src/commands/phases/auth-phase.ts
|
|
5469
|
-
var
|
|
6146
|
+
var import_chalk16 = __toESM(require("chalk"));
|
|
5470
6147
|
init_lib();
|
|
5471
6148
|
|
|
5472
6149
|
// src/commands/utils/auth-helpers.ts
|
|
5473
|
-
var
|
|
6150
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
5474
6151
|
init_lib();
|
|
5475
6152
|
async function handleRegistrationGate() {
|
|
5476
6153
|
console.log();
|
|
5477
|
-
console.log(
|
|
5478
|
-
console.log(
|
|
6154
|
+
console.log(import_chalk15.default.yellow(" \u{1F389} You've made 5 free videos!"));
|
|
6155
|
+
console.log(import_chalk15.default.white(" Sign in to continue creating videos."));
|
|
5479
6156
|
console.log();
|
|
5480
|
-
console.log(
|
|
5481
|
-
console.log(
|
|
5482
|
-
console.log(
|
|
5483
|
-
console.log(
|
|
6157
|
+
console.log(import_chalk15.default.dim(" Benefits of signing in:"));
|
|
6158
|
+
console.log(import_chalk15.default.dim(" \u2022 20 more videos per month"));
|
|
6159
|
+
console.log(import_chalk15.default.dim(" \u2022 Keep your video history"));
|
|
6160
|
+
console.log(import_chalk15.default.dim(" \u2022 Upgrade to Pro anytime"));
|
|
5484
6161
|
let email = await browserLogin();
|
|
5485
6162
|
if (!email) {
|
|
5486
6163
|
console.log();
|
|
5487
6164
|
email = await promptInput("Email: ");
|
|
5488
6165
|
}
|
|
5489
6166
|
if (!email || !email.includes("@")) {
|
|
5490
|
-
console.log(
|
|
6167
|
+
console.log(import_chalk15.default.red(" Invalid email. Run qp again to try again."));
|
|
5491
6168
|
return null;
|
|
5492
6169
|
}
|
|
5493
6170
|
const registerTask = task("Sending verification email");
|
|
@@ -5502,10 +6179,10 @@ async function handleRegistrationGate() {
|
|
|
5502
6179
|
}
|
|
5503
6180
|
registerTask.succeed("Check your email!");
|
|
5504
6181
|
console.log();
|
|
5505
|
-
console.log(
|
|
5506
|
-
console.log(
|
|
6182
|
+
console.log(import_chalk15.default.cyan(" \u{1F4E7} Verification email sent to:"));
|
|
6183
|
+
console.log(import_chalk15.default.white(` ${email}`));
|
|
5507
6184
|
console.log();
|
|
5508
|
-
console.log(
|
|
6185
|
+
console.log(import_chalk15.default.dim(" Click the link in the email, then press Enter to continue..."));
|
|
5509
6186
|
await promptInput("");
|
|
5510
6187
|
const verifyTask = task("Checking verification");
|
|
5511
6188
|
const verified = await isVerified(email);
|
|
@@ -5515,10 +6192,10 @@ async function handleRegistrationGate() {
|
|
|
5515
6192
|
}
|
|
5516
6193
|
verifyTask.fail("Not verified yet");
|
|
5517
6194
|
console.log();
|
|
5518
|
-
console.log(
|
|
5519
|
-
console.log(
|
|
6195
|
+
console.log(import_chalk15.default.yellow(" Email not verified. Check your inbox and spam folder."));
|
|
6196
|
+
console.log(import_chalk15.default.dim(" Run qp again after clicking the verification link."));
|
|
5520
6197
|
console.log();
|
|
5521
|
-
console.log(
|
|
6198
|
+
console.log(import_chalk15.default.cyan(` Or register/upgrade at: ${getUpgradeUrl(email)}`));
|
|
5522
6199
|
console.log();
|
|
5523
6200
|
return { email, verified: false };
|
|
5524
6201
|
}
|
|
@@ -5546,7 +6223,7 @@ async function runAuthPhase(input) {
|
|
|
5546
6223
|
} else {
|
|
5547
6224
|
verifyTask.fail("Email not verified");
|
|
5548
6225
|
console.log();
|
|
5549
|
-
console.log(
|
|
6226
|
+
console.log(import_chalk16.default.yellow(" Check your inbox for the verification email."));
|
|
5550
6227
|
console.log();
|
|
5551
6228
|
const input2 = await promptInput(`New email or Enter to retry (${userEmail}): `);
|
|
5552
6229
|
if (input2.toLowerCase() === "exit") {
|
|
@@ -5571,8 +6248,8 @@ async function runAuthPhase(input) {
|
|
|
5571
6248
|
registerTask.succeed("Check your email!");
|
|
5572
6249
|
await saveUserData({ email: newEmail, verified: false });
|
|
5573
6250
|
console.log();
|
|
5574
|
-
console.log(
|
|
5575
|
-
console.log(
|
|
6251
|
+
console.log(import_chalk16.default.cyan(` \u{1F4E7} Verification sent to: ${newEmail}`));
|
|
6252
|
+
console.log(import_chalk16.default.dim(" Click the link in the email, then run qp again."));
|
|
5576
6253
|
console.log();
|
|
5577
6254
|
gracefulExit();
|
|
5578
6255
|
return null;
|
|
@@ -5588,7 +6265,7 @@ async function runAuthPhase(input) {
|
|
|
5588
6265
|
} else {
|
|
5589
6266
|
recheckTask.fail("Still not verified");
|
|
5590
6267
|
console.log();
|
|
5591
|
-
console.log(
|
|
6268
|
+
console.log(import_chalk16.default.cyan(` Register/upgrade at: ${getUpgradeUrl(userEmail)}`));
|
|
5592
6269
|
console.log();
|
|
5593
6270
|
gracefulExit();
|
|
5594
6271
|
return null;
|
|
@@ -5615,8 +6292,8 @@ async function runAuthPhase(input) {
|
|
|
5615
6292
|
} else {
|
|
5616
6293
|
const remaining = FREE_VIDEO_LIMIT - localVideoCount;
|
|
5617
6294
|
console.log();
|
|
5618
|
-
console.log(
|
|
5619
|
-
console.log(
|
|
6295
|
+
console.log(import_chalk16.default.cyan(` \u{1F3AC} Video ${localVideoCount + 1}/${FREE_VIDEO_LIMIT} free`));
|
|
6296
|
+
console.log(import_chalk16.default.dim(` ${remaining - 1} more after this \u2022 Sign up for unlimited`));
|
|
5620
6297
|
userEmail = await setupUserEmail();
|
|
5621
6298
|
let registeredVerified = false;
|
|
5622
6299
|
try {
|
|
@@ -5635,18 +6312,18 @@ async function runAuthPhase(input) {
|
|
|
5635
6312
|
}
|
|
5636
6313
|
|
|
5637
6314
|
// src/commands/phases/plan-phase.ts
|
|
5638
|
-
var
|
|
5639
|
-
var
|
|
5640
|
-
var
|
|
5641
|
-
var
|
|
6315
|
+
var import_node_fs17 = require("fs");
|
|
6316
|
+
var import_promises16 = require("fs/promises");
|
|
6317
|
+
var import_node_path23 = require("path");
|
|
6318
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
5642
6319
|
|
|
5643
6320
|
// src/ai/fix.ts
|
|
5644
6321
|
init_lib();
|
|
5645
6322
|
async function callGroqAndFinalize(systemPrompt, userMessage, plan, logError2, userEmail, tier = "free") {
|
|
5646
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
5647
|
-
const
|
|
5648
|
-
if (!
|
|
5649
|
-
return finalizePlan(
|
|
6323
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
|
|
6324
|
+
const result = await parseAIJsonWithRepair(content, { logError: logError2, userEmail, tier, context: "Plan Fix" });
|
|
6325
|
+
if (!Array.isArray(result.steps)) throw new Error("AI response has no steps array");
|
|
6326
|
+
return finalizePlan(result, plan);
|
|
5650
6327
|
}
|
|
5651
6328
|
function finalizePlan(result, original) {
|
|
5652
6329
|
result.url = original.url;
|
|
@@ -5770,10 +6447,15 @@ async function translatePlan(plan, targetLang, logError2, userEmail, tier = "fre
|
|
|
5770
6447
|
${JSON.stringify(toTranslate)}
|
|
5771
6448
|
|
|
5772
6449
|
Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
5773
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
5774
|
-
const
|
|
5775
|
-
|
|
5776
|
-
|
|
6450
|
+
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier, "fix");
|
|
6451
|
+
const translated = await parseAIJsonWithRepair(content, {
|
|
6452
|
+
logError: logError2,
|
|
6453
|
+
userEmail,
|
|
6454
|
+
tier,
|
|
6455
|
+
shape: "The JSON should have: description (string), captions (array of strings).",
|
|
6456
|
+
context: "Translate Plan"
|
|
6457
|
+
});
|
|
6458
|
+
if (!Array.isArray(translated.captions)) throw new Error("Translation response has no captions array");
|
|
5777
6459
|
const translatedPlan = JSON.parse(JSON.stringify(plan));
|
|
5778
6460
|
translatedPlan.title = plan.title;
|
|
5779
6461
|
translatedPlan.description = translated.description || plan.description;
|
|
@@ -5790,14 +6472,24 @@ Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
|
5790
6472
|
// src/commands/phases/plan-phase.ts
|
|
5791
6473
|
var INDENT6 = " ";
|
|
5792
6474
|
async function runPlanPhase(params) {
|
|
5793
|
-
const {
|
|
6475
|
+
const {
|
|
6476
|
+
url,
|
|
6477
|
+
config,
|
|
6478
|
+
userEmail,
|
|
6479
|
+
description,
|
|
6480
|
+
planPath,
|
|
6481
|
+
legacyPlanPath,
|
|
6482
|
+
targetLang,
|
|
6483
|
+
userTier = "free",
|
|
6484
|
+
fresh = false
|
|
6485
|
+
} = params;
|
|
5794
6486
|
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
5795
6487
|
const extensionPath = config.browser?.extension;
|
|
5796
6488
|
let existingPlan = null;
|
|
5797
6489
|
let existingPlanPath = null;
|
|
5798
|
-
if ((0,
|
|
6490
|
+
if ((0, import_node_fs17.existsSync)(planPath)) {
|
|
5799
6491
|
try {
|
|
5800
|
-
const parsed = JSON.parse(await (0,
|
|
6492
|
+
const parsed = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
5801
6493
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5802
6494
|
existingPlan = parsed;
|
|
5803
6495
|
existingPlanPath = planPath;
|
|
@@ -5807,12 +6499,12 @@ async function runPlanPhase(params) {
|
|
|
5807
6499
|
}
|
|
5808
6500
|
if (!existingPlan) {
|
|
5809
6501
|
try {
|
|
5810
|
-
const files = (0,
|
|
5811
|
-
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0,
|
|
6502
|
+
const files = (0, import_node_fs17.readdirSync)(config.output.dir);
|
|
6503
|
+
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
|
|
5812
6504
|
for (const file of planFiles) {
|
|
5813
|
-
const filePath = (0,
|
|
6505
|
+
const filePath = (0, import_node_path23.join)(config.output.dir, file);
|
|
5814
6506
|
try {
|
|
5815
|
-
const parsed = JSON.parse(await (0,
|
|
6507
|
+
const parsed = JSON.parse(await (0, import_promises16.readFile)(filePath, "utf-8"));
|
|
5816
6508
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5817
6509
|
existingPlan = parsed;
|
|
5818
6510
|
existingPlanPath = filePath;
|
|
@@ -5824,9 +6516,9 @@ async function runPlanPhase(params) {
|
|
|
5824
6516
|
} catch {
|
|
5825
6517
|
}
|
|
5826
6518
|
}
|
|
5827
|
-
if (!existingPlan && (0,
|
|
6519
|
+
if (!existingPlan && (0, import_node_fs17.existsSync)(legacyPlanPath)) {
|
|
5828
6520
|
try {
|
|
5829
|
-
const parsed = JSON.parse(await (0,
|
|
6521
|
+
const parsed = JSON.parse(await (0, import_promises16.readFile)(legacyPlanPath, "utf-8"));
|
|
5830
6522
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
5831
6523
|
existingPlan = parsed;
|
|
5832
6524
|
existingPlanPath = legacyPlanPath;
|
|
@@ -5836,6 +6528,11 @@ async function runPlanPhase(params) {
|
|
|
5836
6528
|
}
|
|
5837
6529
|
const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
5838
6530
|
const urlMatches = existingPlan && normalizeUrl4(existingPlan.url) === normalizeUrl4(url);
|
|
6531
|
+
if (existingPlan && !urlMatches && isAutoYes()) {
|
|
6532
|
+
console.log();
|
|
6533
|
+
console.log(import_chalk17.default.dim(` Existing plan is for ${existingPlan.url}, not ${url} \u2014 regenerating.`));
|
|
6534
|
+
existingPlan = null;
|
|
6535
|
+
}
|
|
5839
6536
|
let plan;
|
|
5840
6537
|
const doTranslate = async (failMsg) => {
|
|
5841
6538
|
const translateTask = task(`Translating to ${targetLang}`);
|
|
@@ -5850,25 +6547,25 @@ async function runPlanPhase(params) {
|
|
|
5850
6547
|
translateTask.fail(failMsg);
|
|
5851
6548
|
}
|
|
5852
6549
|
};
|
|
5853
|
-
if (existingPlan && existingPlanPath) {
|
|
6550
|
+
if (existingPlan && existingPlanPath && !fresh) {
|
|
5854
6551
|
console.log();
|
|
5855
|
-
console.log(
|
|
5856
|
-
console.log(
|
|
6552
|
+
console.log(import_chalk17.default.dim(` Found existing plan: `) + import_chalk17.default.cyan(formatClickablePath(existingPlanPath)));
|
|
6553
|
+
console.log(import_chalk17.default.dim(` ${existingPlan.steps.length} steps for ${existingPlan.url}`));
|
|
5857
6554
|
if (!urlMatches) {
|
|
5858
|
-
console.log(
|
|
5859
|
-
console.log(
|
|
5860
|
-
console.log(
|
|
6555
|
+
console.log(import_chalk17.default.yellow(` \u26A0 Plan URL differs from argument:`));
|
|
6556
|
+
console.log(import_chalk17.default.yellow(` Plan: ${existingPlan.url}`));
|
|
6557
|
+
console.log(import_chalk17.default.yellow(` Arg: ${url}`));
|
|
5861
6558
|
}
|
|
5862
6559
|
const existingLang = existingPlan.lang || "en";
|
|
5863
6560
|
if (existingLang !== targetLang) {
|
|
5864
|
-
console.log(
|
|
6561
|
+
console.log(import_chalk17.default.yellow(` \u26A0 Plan is in ${existingLang}, config is ${targetLang}`));
|
|
5865
6562
|
}
|
|
5866
6563
|
const reuse = await confirm(`${INDENT6}Use existing plan?`);
|
|
5867
6564
|
if (reuse) {
|
|
5868
6565
|
plan = await validateAndFixPlan(existingPlanPath, task, logError, userEmail, userTier);
|
|
5869
6566
|
if (existingPlanPath !== planPath) {
|
|
5870
6567
|
await savePlan(plan, planPath);
|
|
5871
|
-
console.log(
|
|
6568
|
+
console.log(import_chalk17.default.dim(` Migrated to: ${formatClickablePath(planPath)}`));
|
|
5872
6569
|
}
|
|
5873
6570
|
if (existingLang !== targetLang) {
|
|
5874
6571
|
const translate = await confirm(`${INDENT6}Translate to ${targetLang}?`);
|
|
@@ -5886,7 +6583,8 @@ async function runPlanPhase(params) {
|
|
|
5886
6583
|
userEmail,
|
|
5887
6584
|
extensionPath,
|
|
5888
6585
|
config.output.dir,
|
|
5889
|
-
userTier
|
|
6586
|
+
userTier,
|
|
6587
|
+
{ width: config.video.width, height: config.video.height }
|
|
5890
6588
|
);
|
|
5891
6589
|
displayRateLimitInfo();
|
|
5892
6590
|
if (urlMatches) {
|
|
@@ -5903,7 +6601,7 @@ async function runPlanPhase(params) {
|
|
|
5903
6601
|
uploadIdx++;
|
|
5904
6602
|
}
|
|
5905
6603
|
}
|
|
5906
|
-
console.log(
|
|
6604
|
+
console.log(import_chalk17.default.dim(` Preserved upload paths from previous plan`));
|
|
5907
6605
|
}
|
|
5908
6606
|
await savePlan(plan, planPath);
|
|
5909
6607
|
plan = await validateAndFixPlan(planPath, task, logError, userEmail, userTier);
|
|
@@ -5919,7 +6617,9 @@ async function runPlanPhase(params) {
|
|
|
5919
6617
|
logError,
|
|
5920
6618
|
userEmail,
|
|
5921
6619
|
extensionPath,
|
|
5922
|
-
config.output.dir
|
|
6620
|
+
config.output.dir,
|
|
6621
|
+
void 0,
|
|
6622
|
+
{ width: config.video.width, height: config.video.height }
|
|
5923
6623
|
);
|
|
5924
6624
|
displayRateLimitInfo();
|
|
5925
6625
|
await savePlan(plan, planPath);
|
|
@@ -5927,8 +6627,8 @@ async function runPlanPhase(params) {
|
|
|
5927
6627
|
}
|
|
5928
6628
|
displayPlanSummary(plan);
|
|
5929
6629
|
console.log();
|
|
5930
|
-
console.log(
|
|
5931
|
-
console.log(
|
|
6630
|
+
console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
6631
|
+
console.log(import_chalk17.default.dim(` Enter a prompt to refine, or press Enter to continue.`));
|
|
5932
6632
|
const MAX_REFINEMENTS = 10;
|
|
5933
6633
|
let refinementCount = 0;
|
|
5934
6634
|
while (true) {
|
|
@@ -5946,15 +6646,15 @@ async function runPlanPhase(params) {
|
|
|
5946
6646
|
refineTask.succeed("Plan refined");
|
|
5947
6647
|
displayRateLimitInfo();
|
|
5948
6648
|
if (plan.steps.length > maxSteps) {
|
|
5949
|
-
console.log(
|
|
6649
|
+
console.log(import_chalk17.default.yellow(` \u26A0 ${plan.steps.length} steps exceeds limit of ${maxSteps}`));
|
|
5950
6650
|
}
|
|
5951
6651
|
displayPlanSummary(plan);
|
|
5952
6652
|
console.log();
|
|
5953
|
-
console.log(
|
|
6653
|
+
console.log(import_chalk17.default.dim(` Plan: ${formatClickablePath(planPath)}`));
|
|
5954
6654
|
if (refinementCount >= MAX_REFINEMENTS) {
|
|
5955
6655
|
console.log();
|
|
5956
|
-
console.log(
|
|
5957
|
-
console.log(
|
|
6656
|
+
console.log(import_chalk17.default.yellow(` \u26A0 You've refined the plan ${MAX_REFINEMENTS} times.`));
|
|
6657
|
+
console.log(import_chalk17.default.dim(` Further refinements may incur additional costs.`));
|
|
5958
6658
|
const continueRefining = await confirm(`${INDENT6}Continue refining?`);
|
|
5959
6659
|
if (!continueRefining) break;
|
|
5960
6660
|
}
|
|
@@ -5966,12 +6666,12 @@ async function runPlanPhase(params) {
|
|
|
5966
6666
|
const planLang = plan.lang || "en";
|
|
5967
6667
|
if (planLang !== targetLang) {
|
|
5968
6668
|
let useExistingTranslation = false;
|
|
5969
|
-
if ((0,
|
|
6669
|
+
if ((0, import_node_fs17.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
5970
6670
|
try {
|
|
5971
|
-
const translatedPlan = JSON.parse(await (0,
|
|
6671
|
+
const translatedPlan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
5972
6672
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
5973
6673
|
console.log();
|
|
5974
|
-
console.log(
|
|
6674
|
+
console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
5975
6675
|
const useTranslated = await confirm(`${INDENT6}Use ${targetLang} translation?`);
|
|
5976
6676
|
if (useTranslated) {
|
|
5977
6677
|
plan = translatedPlan;
|
|
@@ -5983,7 +6683,7 @@ async function runPlanPhase(params) {
|
|
|
5983
6683
|
}
|
|
5984
6684
|
if (!useExistingTranslation) {
|
|
5985
6685
|
console.log();
|
|
5986
|
-
console.log(
|
|
6686
|
+
console.log(import_chalk17.default.yellow(` Plan is in ${planLang}, but voice is set to ${targetLang}`));
|
|
5987
6687
|
const translate = await confirm(`${INDENT6}Translate captions to ${targetLang}?`);
|
|
5988
6688
|
if (translate) await doTranslate("Translation failed, using original");
|
|
5989
6689
|
}
|
|
@@ -5992,8 +6692,8 @@ async function runPlanPhase(params) {
|
|
|
5992
6692
|
}
|
|
5993
6693
|
|
|
5994
6694
|
// src/commands/phases/validation-phase.ts
|
|
5995
|
-
var
|
|
5996
|
-
var
|
|
6695
|
+
var import_promises17 = require("fs/promises");
|
|
6696
|
+
var import_chalk18 = __toESM(require("chalk"));
|
|
5997
6697
|
async function runValidationPhase(params) {
|
|
5998
6698
|
let { plan } = params;
|
|
5999
6699
|
const { config, planPath, isExtensionUrl, userEmail, userTier = "free" } = params;
|
|
@@ -6044,21 +6744,21 @@ async function runValidationPhase(params) {
|
|
|
6044
6744
|
const stepNum = result.stepIndex + 1;
|
|
6045
6745
|
const step = plan.steps[result.stepIndex];
|
|
6046
6746
|
console.log();
|
|
6047
|
-
console.log(
|
|
6747
|
+
console.log(import_chalk18.default.red(` \u2717 Step ${stepNum}: ${step.action}`));
|
|
6048
6748
|
const caption = step.caption || "";
|
|
6049
|
-
if (caption) console.log(
|
|
6050
|
-
console.log(
|
|
6051
|
-
if (step.value) console.log(
|
|
6052
|
-
console.log(
|
|
6749
|
+
if (caption) console.log(import_chalk18.default.dim(` Caption: ${caption.slice(0, 50)}${caption.length > 50 ? "..." : ""}`));
|
|
6750
|
+
console.log(import_chalk18.default.dim(` Target: ${step.target || "none"}`));
|
|
6751
|
+
if (step.value) console.log(import_chalk18.default.dim(` Value: ${step.value}`));
|
|
6752
|
+
console.log(import_chalk18.default.dim(` Error: ${result.error}`));
|
|
6053
6753
|
if (result.suggestion) {
|
|
6054
|
-
console.log(
|
|
6754
|
+
console.log(import_chalk18.default.yellow(` Hint: ${result.suggestion}`));
|
|
6055
6755
|
}
|
|
6056
6756
|
console.log();
|
|
6057
6757
|
const isUploadMissingFile = step.action === "upload" && (!step.value || step.value.startsWith("[") || step.value.includes("example") || step.value.includes("/path/to"));
|
|
6058
6758
|
if (isUploadMissingFile) {
|
|
6059
6759
|
const filePath = await promptInput('Enter file path to upload (or "skip"): ');
|
|
6060
6760
|
if (filePath.toLowerCase() === "skip") {
|
|
6061
|
-
console.log(
|
|
6761
|
+
console.log(import_chalk18.default.yellow(` Skipping step ${stepNum}`));
|
|
6062
6762
|
continue;
|
|
6063
6763
|
}
|
|
6064
6764
|
if (filePath) {
|
|
@@ -6067,13 +6767,13 @@ async function runValidationPhase(params) {
|
|
|
6067
6767
|
const revalidation2 = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
6068
6768
|
const newResult2 = revalidation2.results[result.stepIndex];
|
|
6069
6769
|
if (newResult2?.success) {
|
|
6070
|
-
console.log(
|
|
6770
|
+
console.log(import_chalk18.default.green(` \u2713 Step ${stepNum} now passes`));
|
|
6071
6771
|
validationResult = revalidation2;
|
|
6072
6772
|
continue;
|
|
6073
6773
|
}
|
|
6074
6774
|
result.error = newResult2?.error;
|
|
6075
6775
|
result.suggestion = newResult2?.suggestion;
|
|
6076
|
-
console.log(
|
|
6776
|
+
console.log(import_chalk18.default.red(` Still failing: ${result.error}`));
|
|
6077
6777
|
}
|
|
6078
6778
|
}
|
|
6079
6779
|
const fixTask = task(`Fixing step ${stepNum} with AI`);
|
|
@@ -6095,8 +6795,8 @@ async function runValidationPhase(params) {
|
|
|
6095
6795
|
await savePlan(plan, planPath);
|
|
6096
6796
|
fixTask.succeed("AI suggested a fix");
|
|
6097
6797
|
const fixedStep = plan.steps[result.stepIndex];
|
|
6098
|
-
console.log(
|
|
6099
|
-
if (fixedStep.value) console.log(
|
|
6798
|
+
console.log(import_chalk18.default.dim(` New target: ${fixedStep.target || "none"}`));
|
|
6799
|
+
if (fixedStep.value) console.log(import_chalk18.default.dim(` New value: ${fixedStep.value}`));
|
|
6100
6800
|
} catch {
|
|
6101
6801
|
fixTask.fail("AI could not fix automatically");
|
|
6102
6802
|
}
|
|
@@ -6112,14 +6812,14 @@ async function runValidationPhase(params) {
|
|
|
6112
6812
|
result.error = newResult?.error;
|
|
6113
6813
|
result.suggestion = newResult?.suggestion;
|
|
6114
6814
|
console.log();
|
|
6115
|
-
console.log(
|
|
6815
|
+
console.log(import_chalk18.default.yellow(` Edit step ${stepNum} in: `) + import_chalk18.default.cyan(formatClickablePath(planPath)));
|
|
6116
6816
|
console.log();
|
|
6117
6817
|
const choice = await promptInput('Press Enter after editing, or type "skip" to skip this step: ');
|
|
6118
6818
|
if (choice.toLowerCase() === "skip") {
|
|
6119
|
-
console.log(
|
|
6819
|
+
console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
6120
6820
|
continue;
|
|
6121
6821
|
}
|
|
6122
|
-
plan = JSON.parse(await (0,
|
|
6822
|
+
plan = JSON.parse(await (0, import_promises17.readFile)(planPath, "utf-8"));
|
|
6123
6823
|
const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
|
|
6124
6824
|
const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
6125
6825
|
const finalResult = finalRevalidation.results[result.stepIndex];
|
|
@@ -6143,31 +6843,31 @@ async function runValidationPhase(params) {
|
|
|
6143
6843
|
}
|
|
6144
6844
|
|
|
6145
6845
|
// src/commands/utils/chrome-extension.ts
|
|
6146
|
-
var
|
|
6846
|
+
var import_node_fs18 = require("fs");
|
|
6147
6847
|
var import_node_os3 = require("os");
|
|
6148
|
-
var
|
|
6848
|
+
var import_node_path24 = require("path");
|
|
6149
6849
|
function resolveChromeExtension(extensionId) {
|
|
6150
6850
|
const platform = process.platform;
|
|
6151
6851
|
const possibleDirs = [];
|
|
6152
6852
|
const getChromeExtDirs = (userDataPath) => {
|
|
6153
6853
|
const dirs = [];
|
|
6154
6854
|
try {
|
|
6155
|
-
const profiles = (0,
|
|
6855
|
+
const profiles = (0, import_node_fs18.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
6156
6856
|
for (const profile of profiles) {
|
|
6157
|
-
dirs.push((0,
|
|
6857
|
+
dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
|
|
6158
6858
|
}
|
|
6159
6859
|
} catch {
|
|
6160
|
-
dirs.push((0,
|
|
6860
|
+
dirs.push((0, import_node_path24.join)(userDataPath, "Default", "Extensions"));
|
|
6161
6861
|
}
|
|
6162
6862
|
return dirs;
|
|
6163
6863
|
};
|
|
6164
6864
|
if (platform === "win32") {
|
|
6165
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6865
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "AppData", "Local", "Google", "Chrome", "User Data")));
|
|
6166
6866
|
} else if (platform === "darwin") {
|
|
6167
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6867
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
6168
6868
|
} else {
|
|
6169
6869
|
try {
|
|
6170
|
-
const windowsUsers = (0,
|
|
6870
|
+
const windowsUsers = (0, import_node_fs18.readdirSync)("/mnt/c/Users").filter(
|
|
6171
6871
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
6172
6872
|
);
|
|
6173
6873
|
for (const user of windowsUsers) {
|
|
@@ -6175,22 +6875,22 @@ function resolveChromeExtension(extensionId) {
|
|
|
6175
6875
|
}
|
|
6176
6876
|
} catch {
|
|
6177
6877
|
}
|
|
6178
|
-
possibleDirs.push(...getChromeExtDirs((0,
|
|
6878
|
+
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), ".config", "google-chrome")));
|
|
6179
6879
|
}
|
|
6180
6880
|
for (const chromeExtDir of possibleDirs) {
|
|
6181
|
-
const extDir = (0,
|
|
6182
|
-
if (!(0,
|
|
6881
|
+
const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
|
|
6882
|
+
if (!(0, import_node_fs18.existsSync)(extDir)) continue;
|
|
6183
6883
|
try {
|
|
6184
|
-
const versions = (0,
|
|
6884
|
+
const versions = (0, import_node_fs18.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
6185
6885
|
if (versions.length === 0) continue;
|
|
6186
6886
|
const latestVersion = versions.sort().pop();
|
|
6187
|
-
const extPath = (0,
|
|
6887
|
+
const extPath = (0, import_node_path24.join)(extDir, latestVersion);
|
|
6188
6888
|
let name = extensionId;
|
|
6189
6889
|
let popupPath = "popup.html";
|
|
6190
|
-
const manifestPath = (0,
|
|
6191
|
-
if ((0,
|
|
6890
|
+
const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
|
|
6891
|
+
if ((0, import_node_fs18.existsSync)(manifestPath)) {
|
|
6192
6892
|
try {
|
|
6193
|
-
const manifest = JSON.parse((0,
|
|
6893
|
+
const manifest = JSON.parse((0, import_node_fs18.readFileSync)(manifestPath, "utf-8"));
|
|
6194
6894
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
6195
6895
|
name = manifest.name;
|
|
6196
6896
|
}
|
|
@@ -6221,16 +6921,23 @@ async function recordCommand(url, options = {}) {
|
|
|
6221
6921
|
const extensionId = url.slice(7);
|
|
6222
6922
|
chromeExtension = resolveChromeExtension(extensionId);
|
|
6223
6923
|
if (!chromeExtension) {
|
|
6224
|
-
console.log(
|
|
6924
|
+
console.log(import_chalk19.default.red(`
|
|
6225
6925
|
\u2717 Chrome extension not found: ${extensionId}`));
|
|
6226
|
-
console.log(
|
|
6926
|
+
console.log(import_chalk19.default.dim(` Make sure the extension is installed in Chrome
|
|
6227
6927
|
`));
|
|
6228
6928
|
process.exit(1);
|
|
6229
6929
|
}
|
|
6230
6930
|
config.browser = { extension: chromeExtension.path };
|
|
6231
6931
|
url = chromeExtension.popupUrl;
|
|
6232
|
-
console.log(
|
|
6233
|
-
console.log(
|
|
6932
|
+
console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
|
|
6933
|
+
console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
|
|
6934
|
+
}
|
|
6935
|
+
if (!chromeExtension) {
|
|
6936
|
+
const startUrl = resolveStartUrl(url);
|
|
6937
|
+
if (startUrl !== url) {
|
|
6938
|
+
console.log(import_chalk19.default.dim(` Signed-in start page: ${startUrl}`));
|
|
6939
|
+
url = startUrl;
|
|
6940
|
+
}
|
|
6234
6941
|
}
|
|
6235
6942
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
6236
6943
|
const userData = await loadUserData();
|
|
@@ -6243,20 +6950,20 @@ async function recordCommand(url, options = {}) {
|
|
|
6243
6950
|
if (!authResult) return;
|
|
6244
6951
|
const { userEmail, userTier, localVideoCount } = authResult;
|
|
6245
6952
|
if (configCreated) {
|
|
6246
|
-
const configPath = (0,
|
|
6953
|
+
const configPath = (0, import_node_path25.join)(process.cwd(), CONFIG_FILE);
|
|
6247
6954
|
console.log();
|
|
6248
|
-
console.log(
|
|
6249
|
-
console.log(
|
|
6955
|
+
console.log(import_chalk19.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
6956
|
+
console.log(import_chalk19.default.dim(` ${configPath}`));
|
|
6250
6957
|
console.log();
|
|
6251
|
-
console.log(
|
|
6252
|
-
console.log(
|
|
6253
|
-
console.log(
|
|
6254
|
-
console.log(
|
|
6255
|
-
console.log(
|
|
6958
|
+
console.log(import_chalk19.default.cyan(" Review the config file to customize:"));
|
|
6959
|
+
console.log(import_chalk19.default.dim(" \u2022 lang: Voice language (en, es, ar, zh...)"));
|
|
6960
|
+
console.log(import_chalk19.default.dim(" \u2022 video: Resolution, quality, effects"));
|
|
6961
|
+
console.log(import_chalk19.default.dim(" \u2022 music: Background music path and volume"));
|
|
6962
|
+
console.log(import_chalk19.default.dim(" \u2022 output: Output directory"));
|
|
6256
6963
|
console.log();
|
|
6257
6964
|
const proceed = await confirm(`${INDENT3}Proceed with default settings?`);
|
|
6258
6965
|
if (!proceed) {
|
|
6259
|
-
console.log(
|
|
6966
|
+
console.log(import_chalk19.default.yellow(`
|
|
6260
6967
|
Edit ${CONFIG_FILE} and run again.
|
|
6261
6968
|
`));
|
|
6262
6969
|
process.exit(0);
|
|
@@ -6264,17 +6971,17 @@ async function recordCommand(url, options = {}) {
|
|
|
6264
6971
|
}
|
|
6265
6972
|
if (langOverride) config.lang = langOverride;
|
|
6266
6973
|
if (config.music?.path) {
|
|
6267
|
-
const musicPath = (0,
|
|
6268
|
-
if (!(0,
|
|
6974
|
+
const musicPath = (0, import_node_path25.isAbsolute)(config.music.path) ? config.music.path : (0, import_node_path25.resolve)(config.output.dir, config.music.path);
|
|
6975
|
+
if (!(0, import_node_fs19.existsSync)(musicPath)) {
|
|
6269
6976
|
console.log();
|
|
6270
|
-
console.log(
|
|
6271
|
-
console.log(
|
|
6977
|
+
console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
6978
|
+
console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
|
|
6272
6979
|
console.log();
|
|
6273
6980
|
const proceed = await confirm(`${INDENT3}Continue without background music?`);
|
|
6274
6981
|
if (!proceed) {
|
|
6275
|
-
console.log(
|
|
6982
|
+
console.log(import_chalk19.default.yellow(`
|
|
6276
6983
|
Edit music.path in config and run again.`));
|
|
6277
|
-
console.log(
|
|
6984
|
+
console.log(import_chalk19.default.dim(` ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}
|
|
6278
6985
|
`));
|
|
6279
6986
|
process.exit(0);
|
|
6280
6987
|
}
|
|
@@ -6284,10 +6991,10 @@ async function recordCommand(url, options = {}) {
|
|
|
6284
6991
|
const captions = await loadCaptions();
|
|
6285
6992
|
const workDir = await prepareWorkDir();
|
|
6286
6993
|
const targetLang = config.lang || "en";
|
|
6287
|
-
const planPath = (0,
|
|
6288
|
-
const legacyPlanPath = (0,
|
|
6994
|
+
const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
|
|
6995
|
+
const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
|
|
6289
6996
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
6290
|
-
await (0,
|
|
6997
|
+
await (0, import_promises18.mkdir)(config.output.dir, { recursive: true });
|
|
6291
6998
|
await setLogFilePath(config.output.dir);
|
|
6292
6999
|
try {
|
|
6293
7000
|
const plan = await runPlanPhase({
|
|
@@ -6298,7 +7005,8 @@ async function recordCommand(url, options = {}) {
|
|
|
6298
7005
|
...description !== void 0 && { description },
|
|
6299
7006
|
planPath,
|
|
6300
7007
|
legacyPlanPath,
|
|
6301
|
-
targetLang
|
|
7008
|
+
targetLang,
|
|
7009
|
+
...fresh !== void 0 && { fresh }
|
|
6302
7010
|
});
|
|
6303
7011
|
const validatedPlan = await runValidationPhase({
|
|
6304
7012
|
plan,
|
|
@@ -6323,51 +7031,87 @@ async function recordCommand(url, options = {}) {
|
|
|
6323
7031
|
await saveUserData({ localVideoCount: newCount });
|
|
6324
7032
|
const remaining = FREE_VIDEO_LIMIT - newCount;
|
|
6325
7033
|
if (remaining > 0) {
|
|
6326
|
-
console.log(
|
|
7034
|
+
console.log(import_chalk19.default.dim(` ${remaining} free video${remaining === 1 ? "" : "s"} remaining`));
|
|
6327
7035
|
}
|
|
6328
7036
|
}
|
|
6329
7037
|
console.log();
|
|
6330
|
-
console.log(
|
|
7038
|
+
console.log(import_chalk19.default.bold.green(" \u2713 Done!"));
|
|
6331
7039
|
console.log();
|
|
6332
|
-
console.log(
|
|
7040
|
+
console.log(import_chalk19.default.cyan(` ${formatClickablePath(finalPath)}`));
|
|
6333
7041
|
console.log();
|
|
6334
7042
|
return finalPath;
|
|
6335
7043
|
} catch (err) {
|
|
6336
7044
|
await logError("Record Command", err);
|
|
6337
7045
|
if (rethrow) throw err;
|
|
6338
7046
|
console.log();
|
|
6339
|
-
console.log(
|
|
6340
|
-
console.log(
|
|
7047
|
+
console.log(import_chalk19.default.red(` Error: ${err instanceof Error ? err.message : "Unknown error"}`));
|
|
7048
|
+
console.log(import_chalk19.default.dim(` See ${logFilePath} for details`));
|
|
6341
7049
|
process.exit(1);
|
|
6342
7050
|
}
|
|
6343
7051
|
}
|
|
6344
7052
|
|
|
6345
7053
|
// src/commands/script-cmd.ts
|
|
6346
|
-
var
|
|
6347
|
-
var
|
|
6348
|
-
var
|
|
7054
|
+
var import_promises19 = require("fs/promises");
|
|
7055
|
+
var import_node_path26 = require("path");
|
|
7056
|
+
var import_chalk20 = __toESM(require("chalk"));
|
|
6349
7057
|
init_lib();
|
|
7058
|
+
async function reusablePlan(planPath, lines) {
|
|
7059
|
+
try {
|
|
7060
|
+
const plan = JSON.parse(await (0, import_promises19.readFile)(planPath, "utf-8"));
|
|
7061
|
+
if (!Array.isArray(plan.steps)) return null;
|
|
7062
|
+
if (plan.steps.length < lines.length) return null;
|
|
7063
|
+
const matches = lines.every((line, i) => plan.steps[i]?.caption === line);
|
|
7064
|
+
return matches ? plan : null;
|
|
7065
|
+
} catch {
|
|
7066
|
+
return null;
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
6350
7069
|
async function scriptCommand(url, scriptPath, lang) {
|
|
7070
|
+
url = resolveStartUrl(url);
|
|
6351
7071
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
7072
|
+
const userData = await loadUserData();
|
|
7073
|
+
const userTier = userData.email ? await getTierByEmail(userData.email) : "free";
|
|
6352
7074
|
header("Script");
|
|
6353
|
-
const raw = await (0,
|
|
7075
|
+
const raw = await (0, import_promises19.readFile)(scriptPath, "utf-8");
|
|
6354
7076
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
6355
7077
|
if (lines.length === 0) {
|
|
6356
|
-
console.log(
|
|
7078
|
+
console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
|
|
6357
7079
|
process.exit(1);
|
|
6358
7080
|
}
|
|
6359
7081
|
info(`${lines.length} captions from script`);
|
|
7082
|
+
const maxSteps = config.plan?.maxSteps ?? 8;
|
|
7083
|
+
if (lines.length > maxSteps) {
|
|
7084
|
+
console.log(
|
|
7085
|
+
import_chalk20.default.yellow(` \u26A0 ${lines.length} script lines exceeds plan.maxSteps (${maxSteps}) \u2014 recording all of them.`)
|
|
7086
|
+
);
|
|
7087
|
+
console.log(import_chalk20.default.dim(` Raise plan.maxSteps in quickpeek.config.json to silence this, or split the script.`));
|
|
7088
|
+
}
|
|
6360
7089
|
header("Analyze");
|
|
6361
7090
|
const crawlTask = task("Crawling page for UI elements");
|
|
6362
|
-
const { elements } = await crawlPage(url, () => crawlTask);
|
|
7091
|
+
const { elements } = await crawlPage(url, () => crawlTask, findAuthState(url));
|
|
6363
7092
|
const elementsText = elements.slice(0, 30).map((e) => ` ${e.action}: ${e.text} (${e.selector})`).join("\n");
|
|
6364
7093
|
header("Plan");
|
|
6365
7094
|
const planTask = task("Mapping script to UI actions");
|
|
6366
7095
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
6367
|
-
const
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
7096
|
+
const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
7097
|
+
let plan = await reusablePlan(planPath, lines);
|
|
7098
|
+
if (plan) {
|
|
7099
|
+
planTask.succeed(`${plan.steps.length} steps reused from existing plan`);
|
|
7100
|
+
} else {
|
|
7101
|
+
plan = await generatePlanFromScript(
|
|
7102
|
+
lines,
|
|
7103
|
+
url,
|
|
7104
|
+
appTitle,
|
|
7105
|
+
elementsText,
|
|
7106
|
+
lang,
|
|
7107
|
+
logError,
|
|
7108
|
+
userData.email,
|
|
7109
|
+
userTier,
|
|
7110
|
+
() => planTask
|
|
7111
|
+
);
|
|
7112
|
+
await savePlan(plan, planPath);
|
|
7113
|
+
planTask.succeed(`${plan.steps.length} steps planned`);
|
|
7114
|
+
}
|
|
6371
7115
|
header("Voiceover");
|
|
6372
7116
|
const { voiceoverPath, stepAudio } = await generateVoiceover(
|
|
6373
7117
|
plan.steps,
|
|
@@ -6383,10 +7127,10 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
6383
7127
|
const { videoPath } = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
6384
7128
|
header("Compose");
|
|
6385
7129
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
6386
|
-
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config,
|
|
7130
|
+
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, userTier);
|
|
6387
7131
|
console.log();
|
|
6388
|
-
console.log(
|
|
6389
|
-
console.log(
|
|
7132
|
+
console.log(import_chalk20.default.bold.green(" Done!"));
|
|
7133
|
+
console.log(import_chalk20.default.cyan(` ${finalPath}`));
|
|
6390
7134
|
}
|
|
6391
7135
|
|
|
6392
7136
|
// src/qp.ts
|
|
@@ -6456,6 +7200,20 @@ function parseArgs(args) {
|
|
|
6456
7200
|
if (first === "record") {
|
|
6457
7201
|
args = args.slice(1);
|
|
6458
7202
|
}
|
|
7203
|
+
if (first === "edit") {
|
|
7204
|
+
const rest = args.slice(1);
|
|
7205
|
+
let url2;
|
|
7206
|
+
let lang2;
|
|
7207
|
+
for (const arg2 of rest) {
|
|
7208
|
+
if (arg2.startsWith("lang=")) {
|
|
7209
|
+
lang2 = arg2.slice(5);
|
|
7210
|
+
} else if (!arg2.startsWith("-") && !url2 && !arg2.includes("=")) {
|
|
7211
|
+
url2 = normalizeUrl3(arg2);
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
7214
|
+
if (!url2) return { ...base, command: "help" };
|
|
7215
|
+
return { ...base, command: "edit", url: url2, lang: lang2 };
|
|
7216
|
+
}
|
|
6459
7217
|
if (first === "capture") {
|
|
6460
7218
|
const rest = args.slice(1);
|
|
6461
7219
|
let url2;
|
|
@@ -6479,7 +7237,9 @@ function parseArgs(args) {
|
|
|
6479
7237
|
let fresh = false;
|
|
6480
7238
|
for (let i = 0; i < args.length; i++) {
|
|
6481
7239
|
const arg2 = args[i];
|
|
6482
|
-
if (arg2.startsWith("
|
|
7240
|
+
if (!url && (arg2.startsWith("http://") || arg2.startsWith("https://"))) {
|
|
7241
|
+
url = normalizeUrl3(arg2);
|
|
7242
|
+
} else if (arg2.startsWith("lang=")) {
|
|
6483
7243
|
lang = arg2.slice(5);
|
|
6484
7244
|
} else if (arg2 === "--script" || arg2 === "-s") {
|
|
6485
7245
|
scriptPath = args[i + 1];
|
|
@@ -6523,8 +7283,8 @@ async function main() {
|
|
|
6523
7283
|
break;
|
|
6524
7284
|
case "auth":
|
|
6525
7285
|
if (!parsed.url) {
|
|
6526
|
-
console.log(
|
|
6527
|
-
console.log(
|
|
7286
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
7287
|
+
console.log(import_chalk21.default.dim(" Run: qp auth <url>"));
|
|
6528
7288
|
process.exit(1);
|
|
6529
7289
|
}
|
|
6530
7290
|
await authCommand(parsed.url);
|
|
@@ -6535,6 +7295,14 @@ async function main() {
|
|
|
6535
7295
|
case "auth-list":
|
|
6536
7296
|
await authListCommand();
|
|
6537
7297
|
break;
|
|
7298
|
+
case "edit":
|
|
7299
|
+
if (!parsed.url) {
|
|
7300
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
7301
|
+
console.log(import_chalk21.default.dim(" Run: qp edit <url>"));
|
|
7302
|
+
process.exit(1);
|
|
7303
|
+
}
|
|
7304
|
+
await editCommand(parsed.url, parsed.lang);
|
|
7305
|
+
break;
|
|
6538
7306
|
case "init":
|
|
6539
7307
|
await initCommand();
|
|
6540
7308
|
break;
|
|
@@ -6543,8 +7311,8 @@ async function main() {
|
|
|
6543
7311
|
break;
|
|
6544
7312
|
case "record":
|
|
6545
7313
|
if (!parsed.url) {
|
|
6546
|
-
console.log(
|
|
6547
|
-
console.log(
|
|
7314
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
7315
|
+
console.log(import_chalk21.default.dim(" Run: qp --help"));
|
|
6548
7316
|
process.exit(1);
|
|
6549
7317
|
}
|
|
6550
7318
|
setAutoYes(parsed.yes);
|
|
@@ -6563,8 +7331,8 @@ async function main() {
|
|
|
6563
7331
|
break;
|
|
6564
7332
|
case "capture":
|
|
6565
7333
|
if (!parsed.url) {
|
|
6566
|
-
console.log(
|
|
6567
|
-
console.log(
|
|
7334
|
+
console.log(import_chalk21.default.red(" Missing URL"));
|
|
7335
|
+
console.log(import_chalk21.default.dim(" Run: qp --help"));
|
|
6568
7336
|
process.exit(1);
|
|
6569
7337
|
}
|
|
6570
7338
|
console.log();
|