@spark-apps/quickpeek 1.2.1 → 1.2.3
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 +254 -52
- package/dist/mcp-tools.mjs +1295 -603
- package/dist/qp.js +1272 -729
- package/dist/{scraping-CHN4ClyT.d.mts → scraping-C334VxX5.d.mts} +45 -4
- package/dist/web.d.mts +2 -2
- package/dist/web.mjs +64 -24
- package/mcp.mjs +5 -1
- package/package.json +1 -1
package/dist/qp.js
CHANGED
|
@@ -128,6 +128,20 @@ var init_utils = __esm({
|
|
|
128
128
|
function apiKey() {
|
|
129
129
|
return process.env.SPARK_AI_KEY || "";
|
|
130
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
|
+
}
|
|
131
145
|
async function getOrRefreshDirectToken() {
|
|
132
146
|
const now = Date.now();
|
|
133
147
|
if (cachedDirectToken && cachedDirectToken.expiresAt > now + 3e5) {
|
|
@@ -179,8 +193,10 @@ async function getOrRefreshRelayToken(email) {
|
|
|
179
193
|
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
180
194
|
}
|
|
181
195
|
}
|
|
182
|
-
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
183
|
-
|
|
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);
|
|
184
200
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
185
201
|
try {
|
|
186
202
|
const response = await fetch(chatUrl, {
|
|
@@ -196,36 +212,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
196
212
|
]
|
|
197
213
|
})
|
|
198
214
|
});
|
|
215
|
+
const bodyText = await response.text().catch(() => "");
|
|
199
216
|
let data = {};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
try {
|
|
218
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
219
|
+
} catch {
|
|
203
220
|
}
|
|
204
221
|
if (!response.ok) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 = {
|
|
210
231
|
success: false,
|
|
211
232
|
error,
|
|
212
|
-
isRateLimit:
|
|
213
|
-
|
|
233
|
+
isRateLimit: response.status === 429,
|
|
234
|
+
status: response.status,
|
|
235
|
+
host,
|
|
236
|
+
...detail ? { detail } : {},
|
|
237
|
+
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
214
238
|
};
|
|
215
239
|
await new Promise((resolve9) => setTimeout(resolve9, Math.min(delayMs, 3e4)));
|
|
216
240
|
continue;
|
|
217
241
|
}
|
|
242
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
243
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
218
244
|
return {
|
|
219
245
|
success: false,
|
|
220
246
|
error,
|
|
221
247
|
isRateLimit: response.status === 429,
|
|
222
|
-
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 } : {}
|
|
223
253
|
};
|
|
224
254
|
}
|
|
225
255
|
if (!data.success || !data.response) {
|
|
226
256
|
return {
|
|
227
257
|
success: false,
|
|
228
|
-
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) } : {}
|
|
229
262
|
};
|
|
230
263
|
}
|
|
231
264
|
return {
|
|
@@ -239,14 +272,16 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
239
272
|
}
|
|
240
273
|
};
|
|
241
274
|
} catch (err) {
|
|
242
|
-
if (attempt === 3)
|
|
275
|
+
if (attempt === 3) {
|
|
276
|
+
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
277
|
+
}
|
|
243
278
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
244
279
|
await new Promise((resolve9) => setTimeout(resolve9, delayMs));
|
|
245
280
|
}
|
|
246
281
|
}
|
|
247
|
-
return
|
|
282
|
+
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
248
283
|
}
|
|
249
|
-
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
284
|
+
async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
250
285
|
if (!apiKey()) {
|
|
251
286
|
return {
|
|
252
287
|
success: false,
|
|
@@ -256,12 +291,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
|
256
291
|
}
|
|
257
292
|
try {
|
|
258
293
|
const token = await getOrRefreshDirectToken();
|
|
259
|
-
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
294
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
|
|
260
295
|
} catch (err) {
|
|
261
296
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
262
297
|
}
|
|
263
298
|
}
|
|
264
|
-
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
299
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
|
|
265
300
|
if (!email) {
|
|
266
301
|
return {
|
|
267
302
|
success: false,
|
|
@@ -271,11 +306,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
|
271
306
|
}
|
|
272
307
|
try {
|
|
273
308
|
let token = await getOrRefreshRelayToken(email);
|
|
274
|
-
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
309
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
275
310
|
if (!result.success && result.isAuthError) {
|
|
276
311
|
cachedRelayToken = null;
|
|
277
312
|
token = await getOrRefreshRelayToken(email);
|
|
278
|
-
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
313
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
279
314
|
}
|
|
280
315
|
return result;
|
|
281
316
|
} catch (err) {
|
|
@@ -307,11 +342,13 @@ function normalizeUrl(input) {
|
|
|
307
342
|
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
308
343
|
return `https://${url}`;
|
|
309
344
|
}
|
|
310
|
-
var SPARK_AI_BASE, SPARK_AI_CHAT, SPARK_AI_TOKEN, RELAY_BASE, RELAY_TOKEN_URL, RELAY_CHAT_URL, cachedDirectToken, cachedRelayToken;
|
|
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;
|
|
311
346
|
var init_api_client = __esm({
|
|
312
347
|
"src/lib/api-client.ts"() {
|
|
313
348
|
"use strict";
|
|
314
349
|
init_utils();
|
|
350
|
+
SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
351
|
+
CLI_BACKOFF_MS = [1e4, 3e4];
|
|
315
352
|
SPARK_AI_BASE = "https://sparkbrain.app";
|
|
316
353
|
SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
317
354
|
SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
@@ -342,7 +379,7 @@ async function getStatusCached(email) {
|
|
|
342
379
|
function getTier(d) {
|
|
343
380
|
if (!d.subscription || !d.registered) return "free";
|
|
344
381
|
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
345
|
-
return TIERS[d.subscription.price_id] ?? "
|
|
382
|
+
return TIERS[d.subscription.price_id] ?? "pro";
|
|
346
383
|
}
|
|
347
384
|
async function getTierByEmail(email) {
|
|
348
385
|
try {
|
|
@@ -359,17 +396,18 @@ async function isVerified(email) {
|
|
|
359
396
|
}
|
|
360
397
|
}
|
|
361
398
|
function pricingUrl(opts) {
|
|
362
|
-
const u = new URL(`${BASE}
|
|
399
|
+
const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
|
|
363
400
|
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
364
401
|
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
365
402
|
return u.toString();
|
|
366
403
|
}
|
|
367
|
-
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;
|
|
368
405
|
var init_SparkStripe_feature_gating = __esm({
|
|
369
406
|
"src/lib/SparkStripe-feature-gating.ts"() {
|
|
370
407
|
"use strict";
|
|
371
408
|
APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
372
|
-
|
|
409
|
+
OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
|
|
410
|
+
BASE = "https://sparkpay.dev";
|
|
373
411
|
TIERS = {
|
|
374
412
|
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
375
413
|
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
@@ -421,7 +459,7 @@ var VERSION, CONFIG_FILE, DEFAULT_CONFIG, DEFAULT_CAPTIONS;
|
|
|
421
459
|
var init_config = __esm({
|
|
422
460
|
"src/lib/config.ts"() {
|
|
423
461
|
"use strict";
|
|
424
|
-
VERSION = "1.2.
|
|
462
|
+
VERSION = "1.2.3";
|
|
425
463
|
CONFIG_FILE = "quickpeek.config.json";
|
|
426
464
|
DEFAULT_CONFIG = {
|
|
427
465
|
plan: { maxSteps: 8 },
|
|
@@ -571,6 +609,9 @@ var init_scraping = __esm({
|
|
|
571
609
|
});
|
|
572
610
|
|
|
573
611
|
// src/lib/crawl.ts
|
|
612
|
+
function idSelector(id) {
|
|
613
|
+
return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
|
|
614
|
+
}
|
|
574
615
|
function getActionForType(type) {
|
|
575
616
|
switch (type) {
|
|
576
617
|
case "button":
|
|
@@ -596,7 +637,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
596
637
|
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
597
638
|
}
|
|
598
639
|
const browserTask = task2("Launching browser");
|
|
599
|
-
const browser = await
|
|
640
|
+
const browser = await import_playwright3.chromium.launch({ headless: true });
|
|
600
641
|
const context = await browser.newContext({
|
|
601
642
|
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
602
643
|
// Crawl at the size we will record at, or the measurements below describe
|
|
@@ -700,9 +741,9 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
700
741
|
} else if (el.isFile || el.type === "file") {
|
|
701
742
|
selector = 'input[type="file"]';
|
|
702
743
|
} else if (el.type === "range") {
|
|
703
|
-
selector = el.id ?
|
|
744
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
704
745
|
} else if (el.type === "checkbox") {
|
|
705
|
-
selector = el.id ?
|
|
746
|
+
selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
|
|
706
747
|
} else if (el.type === "button") {
|
|
707
748
|
if (btnTexts[el.text] === 1) {
|
|
708
749
|
selector = `text="${el.text}"`;
|
|
@@ -712,7 +753,7 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
712
753
|
} else if (el.type === "link") {
|
|
713
754
|
selector = `text="${el.text}"`;
|
|
714
755
|
} else if (el.id) {
|
|
715
|
-
selector =
|
|
756
|
+
selector = idSelector(el.id);
|
|
716
757
|
} else if (el.name) {
|
|
717
758
|
selector = `[name="${el.name}"]`;
|
|
718
759
|
} else if (el.placeholder) {
|
|
@@ -747,11 +788,11 @@ async function crawlPage(url, task2 = noopTask, storageStatePath, viewport) {
|
|
|
747
788
|
}
|
|
748
789
|
return { title, elements, scrollable };
|
|
749
790
|
}
|
|
750
|
-
var
|
|
791
|
+
var import_playwright3, noopTask, CRAWL_CACHE_TTL_MS, CRAWL_CACHE_MAX_ENTRIES, crawlCache;
|
|
751
792
|
var init_crawl = __esm({
|
|
752
793
|
"src/lib/crawl.ts"() {
|
|
753
794
|
"use strict";
|
|
754
|
-
|
|
795
|
+
import_playwright3 = require("playwright");
|
|
755
796
|
init_scraping();
|
|
756
797
|
noopTask = () => ({ succeed: () => {
|
|
757
798
|
} });
|
|
@@ -813,9 +854,9 @@ function convertArgToWSL(arg2) {
|
|
|
813
854
|
function spawnFFmpeg(cmd2, args) {
|
|
814
855
|
if (isWindowsRuntime) {
|
|
815
856
|
const wslArgs = args.map(convertArgToWSL);
|
|
816
|
-
return (0,
|
|
857
|
+
return (0, import_node_child_process2.spawn)("wsl", ["-e", cmd2, ...wslArgs]);
|
|
817
858
|
}
|
|
818
|
-
return (0,
|
|
859
|
+
return (0, import_node_child_process2.spawn)(cmd2, args);
|
|
819
860
|
}
|
|
820
861
|
function runFFmpeg(args, errorLabel) {
|
|
821
862
|
return new Promise((resolve9, reject) => {
|
|
@@ -831,11 +872,11 @@ function runFFmpeg(args, errorLabel) {
|
|
|
831
872
|
proc.on("error", reject);
|
|
832
873
|
});
|
|
833
874
|
}
|
|
834
|
-
var
|
|
875
|
+
var import_node_child_process2, isWindowsRuntime;
|
|
835
876
|
var init_spawn = __esm({
|
|
836
877
|
"src/lib/ffmpeg/spawn.ts"() {
|
|
837
878
|
"use strict";
|
|
838
|
-
|
|
879
|
+
import_node_child_process2 = require("child_process");
|
|
839
880
|
init_utils();
|
|
840
881
|
isWindowsRuntime = process.platform === "win32";
|
|
841
882
|
}
|
|
@@ -1016,8 +1057,39 @@ function getMediaDuration(filePath) {
|
|
|
1016
1057
|
proc.on("error", reject);
|
|
1017
1058
|
});
|
|
1018
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
|
+
}
|
|
1019
1091
|
function concatMedia(listPath, output) {
|
|
1020
|
-
return runFFmpeg(
|
|
1092
|
+
return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
|
|
1021
1093
|
}
|
|
1022
1094
|
function generateSilence(outputPath, durationMs) {
|
|
1023
1095
|
return runFFmpeg(
|
|
@@ -1266,6 +1338,114 @@ var init_ffmpeg2 = __esm({
|
|
|
1266
1338
|
}
|
|
1267
1339
|
});
|
|
1268
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
|
+
|
|
1269
1449
|
// src/lib/prompts.ts
|
|
1270
1450
|
function buildSystemPrompt(maxSteps) {
|
|
1271
1451
|
let scope;
|
|
@@ -1467,7 +1647,7 @@ var init_styles = __esm({
|
|
|
1467
1647
|
async function registerFreeUser(email) {
|
|
1468
1648
|
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1469
1649
|
try {
|
|
1470
|
-
const response = await fetch(`https://
|
|
1650
|
+
const response = await fetch(`https://sparkpay.dev/api/register-free`, {
|
|
1471
1651
|
method: "POST",
|
|
1472
1652
|
headers: { "Content-Type": "application/json" },
|
|
1473
1653
|
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
@@ -1534,7 +1714,7 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1534
1714
|
return 0;
|
|
1535
1715
|
}
|
|
1536
1716
|
const mediaPath = (0, import_node_path2.isAbsolute)(step.value) ? step.value : (0, import_node_path2.resolve)(baseDir, step.value);
|
|
1537
|
-
if (!(0,
|
|
1717
|
+
if (!(0, import_node_fs3.existsSync)(mediaPath)) {
|
|
1538
1718
|
return 0;
|
|
1539
1719
|
}
|
|
1540
1720
|
try {
|
|
@@ -1556,8 +1736,8 @@ async function getMediaStepDuration(step, baseDir) {
|
|
|
1556
1736
|
async function loadVoiceoverMeta(outputDir) {
|
|
1557
1737
|
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1558
1738
|
try {
|
|
1559
|
-
if (!(0,
|
|
1560
|
-
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");
|
|
1561
1741
|
return JSON.parse(content);
|
|
1562
1742
|
} catch {
|
|
1563
1743
|
return null;
|
|
@@ -1565,7 +1745,7 @@ async function loadVoiceoverMeta(outputDir) {
|
|
|
1565
1745
|
}
|
|
1566
1746
|
async function saveVoiceoverMeta(outputDir, meta) {
|
|
1567
1747
|
const metaPath = (0, import_node_path2.join)(outputDir, "voiceover.json");
|
|
1568
|
-
await (0,
|
|
1748
|
+
await (0, import_promises3.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
1569
1749
|
}
|
|
1570
1750
|
async function concatenateAudio(inputs, output, workDir) {
|
|
1571
1751
|
const formatPath = (f) => {
|
|
@@ -1574,17 +1754,17 @@ async function concatenateAudio(inputs, output, workDir) {
|
|
|
1574
1754
|
};
|
|
1575
1755
|
const listContent = inputs.map((f) => `file '${formatPath(f)}'`).join("\n");
|
|
1576
1756
|
const listPath = (0, import_node_path2.join)(workDir, "concat.txt");
|
|
1577
|
-
await (0,
|
|
1757
|
+
await (0, import_promises3.writeFile)(listPath, listContent);
|
|
1578
1758
|
await concatMedia(listPath, output);
|
|
1579
|
-
await (0,
|
|
1759
|
+
await (0, import_promises3.unlink)(listPath).catch(() => {
|
|
1580
1760
|
});
|
|
1581
1761
|
}
|
|
1582
|
-
var
|
|
1762
|
+
var import_node_fs3, import_promises3, import_node_path2, isWindows;
|
|
1583
1763
|
var init_helpers = __esm({
|
|
1584
1764
|
"src/lib/tts/helpers.ts"() {
|
|
1585
1765
|
"use strict";
|
|
1586
|
-
|
|
1587
|
-
|
|
1766
|
+
import_node_fs3 = require("fs");
|
|
1767
|
+
import_promises3 = require("fs/promises");
|
|
1588
1768
|
import_node_path2 = require("path");
|
|
1589
1769
|
init_ffmpeg2();
|
|
1590
1770
|
init_utils();
|
|
@@ -1633,9 +1813,9 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1633
1813
|
const audioPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}.mp3`);
|
|
1634
1814
|
const originalPaddingPath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-padding.mp3`);
|
|
1635
1815
|
const silencePath = (0, import_node_path3.join)(audioDir, `step-${stepNum}-silence.mp3`);
|
|
1636
|
-
if ((0,
|
|
1816
|
+
if ((0, import_node_fs4.existsSync)(audioPath)) {
|
|
1637
1817
|
segments.push(audioPath);
|
|
1638
|
-
if ((0,
|
|
1818
|
+
if ((0, import_node_fs4.existsSync)(originalPaddingPath)) {
|
|
1639
1819
|
segments.push(originalPaddingPath);
|
|
1640
1820
|
}
|
|
1641
1821
|
if (recordingDuration > voiceDuration) {
|
|
@@ -1643,7 +1823,7 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1643
1823
|
await generateSilence(alignPaddingPath, recordingDuration - voiceDuration);
|
|
1644
1824
|
segments.push(alignPaddingPath);
|
|
1645
1825
|
}
|
|
1646
|
-
} else if ((0,
|
|
1826
|
+
} else if ((0, import_node_fs4.existsSync)(silencePath)) {
|
|
1647
1827
|
if (targetDuration !== voiceDuration) {
|
|
1648
1828
|
await generateSilence(silencePath, targetDuration);
|
|
1649
1829
|
}
|
|
@@ -1667,11 +1847,11 @@ async function alignVoiceoverToRecording(stepAudio, actualRecordingDurations, wo
|
|
|
1667
1847
|
}
|
|
1668
1848
|
return alignedStepAudio;
|
|
1669
1849
|
}
|
|
1670
|
-
var
|
|
1850
|
+
var import_node_fs4, import_node_path3;
|
|
1671
1851
|
var init_timing = __esm({
|
|
1672
1852
|
"src/lib/tts/timing.ts"() {
|
|
1673
1853
|
"use strict";
|
|
1674
|
-
|
|
1854
|
+
import_node_fs4 = require("fs");
|
|
1675
1855
|
import_node_path3 = require("path");
|
|
1676
1856
|
init_ffmpeg2();
|
|
1677
1857
|
init_helpers();
|
|
@@ -1736,6 +1916,19 @@ var init_voices = __esm({
|
|
|
1736
1916
|
|
|
1737
1917
|
// src/lib/tts/synthesize.ts
|
|
1738
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) {
|
|
1739
1932
|
const tempScriptPath = (0, import_node_path5.join)((0, import_node_path5.resolve)(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1740
1933
|
const wslScriptPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(tempScriptPath)) : tempScriptPath;
|
|
1741
1934
|
const wslOutputPath = isWindows ? toWSLPathForTTS((0, import_node_path5.resolve)(outputPath)) : outputPath;
|
|
@@ -1753,44 +1946,49 @@ async def main():
|
|
|
1753
1946
|
asyncio.run(main())
|
|
1754
1947
|
`;
|
|
1755
1948
|
try {
|
|
1756
|
-
await (0,
|
|
1949
|
+
await (0, import_promises4.writeFile)(tempScriptPath, pythonScript);
|
|
1757
1950
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1758
1951
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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)})`);
|
|
1780
1974
|
}
|
|
1975
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1781
1976
|
}
|
|
1782
1977
|
}
|
|
1783
1978
|
}
|
|
1784
|
-
var
|
|
1979
|
+
var import_node_child_process3, import_node_fs5, import_promises4, import_node_path5, import_promises5, import_node_util, execAsync, PYTHON_ATTEMPTS;
|
|
1785
1980
|
var init_synthesize = __esm({
|
|
1786
1981
|
"src/lib/tts/synthesize.ts"() {
|
|
1787
1982
|
"use strict";
|
|
1788
|
-
|
|
1789
|
-
|
|
1983
|
+
import_node_child_process3 = require("child_process");
|
|
1984
|
+
import_node_fs5 = require("fs");
|
|
1985
|
+
import_promises4 = require("fs/promises");
|
|
1790
1986
|
import_node_path5 = require("path");
|
|
1987
|
+
import_promises5 = require("stream/promises");
|
|
1791
1988
|
import_node_util = require("util");
|
|
1792
1989
|
init_helpers();
|
|
1793
|
-
execAsync = (0, import_node_util.promisify)(
|
|
1990
|
+
execAsync = (0, import_node_util.promisify)(import_node_child_process3.exec);
|
|
1991
|
+
PYTHON_ATTEMPTS = 2;
|
|
1794
1992
|
}
|
|
1795
1993
|
});
|
|
1796
1994
|
|
|
@@ -1827,7 +2025,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1827
2025
|
return { voiceoverPath: "", stepAudio: stepAudio2 };
|
|
1828
2026
|
}
|
|
1829
2027
|
const audioDir = (0, import_node_path6.join)(workDir, "audio");
|
|
1830
|
-
await (0,
|
|
2028
|
+
await (0, import_promises6.mkdir)(audioDir, { recursive: true });
|
|
1831
2029
|
const segments = [];
|
|
1832
2030
|
const stepAudio = /* @__PURE__ */ new Map();
|
|
1833
2031
|
const ttsTask = task2("Generating voiceover");
|
|
@@ -1878,7 +2076,7 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1878
2076
|
return { voiceoverPath: "", stepAudio };
|
|
1879
2077
|
}
|
|
1880
2078
|
const concatTask = task2("Combining audio");
|
|
1881
|
-
await (0,
|
|
2079
|
+
await (0, import_promises6.mkdir)(outputDir, { recursive: true });
|
|
1882
2080
|
const voiceoverPath = (0, import_node_path6.join)(outputDir, "voiceover.wav");
|
|
1883
2081
|
await concatenateAudio(segments, voiceoverPath, workDir);
|
|
1884
2082
|
concatTask.succeed("Done");
|
|
@@ -1892,11 +2090,11 @@ async function generateVoiceover(steps, config, workDir, outputDir, logError2, t
|
|
|
1892
2090
|
await saveVoiceoverMeta(outputDir, meta);
|
|
1893
2091
|
return { voiceoverPath, stepAudio };
|
|
1894
2092
|
}
|
|
1895
|
-
var
|
|
2093
|
+
var import_promises6, import_node_path6;
|
|
1896
2094
|
var init_generate = __esm({
|
|
1897
2095
|
"src/lib/tts/generate.ts"() {
|
|
1898
2096
|
"use strict";
|
|
1899
|
-
|
|
2097
|
+
import_promises6 = require("fs/promises");
|
|
1900
2098
|
import_node_path6 = require("path");
|
|
1901
2099
|
init_ffmpeg2();
|
|
1902
2100
|
init_voices();
|
|
@@ -1943,6 +2141,7 @@ var init_types3 = __esm({
|
|
|
1943
2141
|
// src/lib/index.ts
|
|
1944
2142
|
var lib_exports = {};
|
|
1945
2143
|
__export(lib_exports, {
|
|
2144
|
+
CLI_BACKOFF_MS: () => CLI_BACKOFF_MS,
|
|
1946
2145
|
CONFIG_FILE: () => CONFIG_FILE,
|
|
1947
2146
|
CURSOR_CSS: () => CURSOR_CSS,
|
|
1948
2147
|
CURSOR_PNG: () => CURSOR_PNG,
|
|
@@ -1953,9 +2152,11 @@ __export(lib_exports, {
|
|
|
1953
2152
|
FEATURES: () => FEATURES,
|
|
1954
2153
|
INTERACTIVE_SELECTORS: () => INTERACTIVE_SELECTORS,
|
|
1955
2154
|
LANGUAGE_VOICES: () => import_video_kit5.WIDE_VOICES,
|
|
2155
|
+
SERVER_BACKOFF_MS: () => SERVER_BACKOFF_MS,
|
|
1956
2156
|
VERSION: () => VERSION,
|
|
1957
2157
|
alignVoiceoverToRecording: () => alignVoiceoverToRecording,
|
|
1958
2158
|
applyTransitions: () => applyTransitions,
|
|
2159
|
+
buildConcatArgs: () => buildConcatArgs,
|
|
1959
2160
|
buildKeepSegments: () => buildKeepSegments,
|
|
1960
2161
|
buildStepTransitions: () => buildStepTransitions,
|
|
1961
2162
|
buildSystemPrompt: () => buildSystemPrompt,
|
|
@@ -1969,9 +2170,11 @@ __export(lib_exports, {
|
|
|
1969
2170
|
concatVideoWithOutro: () => concatVideoWithOutro,
|
|
1970
2171
|
crawlPage: () => crawlPage,
|
|
1971
2172
|
estimateWordTimings: () => estimateWordTimings,
|
|
2173
|
+
extractJsonCandidates: () => extractJsonCandidates,
|
|
1972
2174
|
generateASSHeader: () => generateASSHeader,
|
|
1973
2175
|
generateSilence: () => generateSilence,
|
|
1974
2176
|
generateVoiceover: () => generateVoiceover,
|
|
2177
|
+
getAudioCodec: () => getAudioCodec,
|
|
1975
2178
|
getErrorMessage: () => getErrorMessage,
|
|
1976
2179
|
getLanguageName: () => getLanguageName,
|
|
1977
2180
|
getMediaDuration: () => getMediaDuration,
|
|
@@ -1982,20 +2185,25 @@ __export(lib_exports, {
|
|
|
1982
2185
|
hasAccess: () => hasAccess,
|
|
1983
2186
|
hasTrialRemaining: () => hasTrialRemaining,
|
|
1984
2187
|
hexToASS: () => import_video_kit.hexToAss,
|
|
2188
|
+
idSelector: () => idSelector,
|
|
1985
2189
|
isCanceling: () => isCanceling,
|
|
1986
2190
|
isExcludedLink: () => isExcludedLink,
|
|
2191
|
+
isHonestWav: () => isHonestWav,
|
|
1987
2192
|
isPaid: () => isPaid,
|
|
1988
2193
|
isVerified: () => isVerified,
|
|
1989
2194
|
loadExistingVoiceover: () => loadExistingVoiceover,
|
|
1990
2195
|
loadVoiceoverMeta: () => loadVoiceoverMeta,
|
|
1991
2196
|
mapTimestampsAfterCompression: () => mapTimestampsAfterCompression,
|
|
1992
2197
|
normalizeUrl: () => normalizeUrl,
|
|
2198
|
+
parseAIJson: () => parseAIJson,
|
|
1993
2199
|
parseAIPlanResponse: () => parseAIPlanResponse,
|
|
1994
2200
|
parsePartial: () => parsePartial,
|
|
1995
2201
|
pricingUrl: () => pricingUrl,
|
|
1996
2202
|
registerFreeUser: () => registerFreeUser,
|
|
2203
|
+
repairUnicodeEscapes: () => repairUnicodeEscapes,
|
|
1997
2204
|
resolveVoice: () => resolveVoice,
|
|
1998
2205
|
rgbaToHex: () => rgbaToHex,
|
|
2206
|
+
salvageTruncatedJson: () => salvageTruncatedJson,
|
|
1999
2207
|
shouldSkipLink: () => shouldSkipLink,
|
|
2000
2208
|
shouldWatermark: () => shouldWatermark,
|
|
2001
2209
|
showPaywall: () => showPaywall,
|
|
@@ -2014,6 +2222,7 @@ var init_lib = __esm({
|
|
|
2014
2222
|
init_crawl();
|
|
2015
2223
|
init_cursor();
|
|
2016
2224
|
init_ffmpeg2();
|
|
2225
|
+
init_json_repair();
|
|
2017
2226
|
init_prompts();
|
|
2018
2227
|
init_SparkStripe_feature_gating();
|
|
2019
2228
|
init_scraping();
|
|
@@ -2032,16 +2241,14 @@ var import_dotenv = __toESM(require("dotenv"));
|
|
|
2032
2241
|
var import_chalk21 = __toESM(require("chalk"));
|
|
2033
2242
|
|
|
2034
2243
|
// src/commands/auth-cmd.ts
|
|
2035
|
-
var import_node_readline2 = require("readline");
|
|
2036
2244
|
var import_chalk3 = __toESM(require("chalk"));
|
|
2037
|
-
var import_playwright2 = require("playwright");
|
|
2038
2245
|
|
|
2039
2246
|
// src/browser/auth-state.ts
|
|
2040
2247
|
var import_node_fs = require("fs");
|
|
2041
2248
|
var import_promises = require("fs/promises");
|
|
2042
2249
|
var import_node_os = require("os");
|
|
2043
2250
|
var import_node_path = require("path");
|
|
2044
|
-
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");
|
|
2045
2252
|
function hostOf(url) {
|
|
2046
2253
|
try {
|
|
2047
2254
|
return new URL(url).host;
|
|
@@ -2053,156 +2260,468 @@ function authStatePath(url) {
|
|
|
2053
2260
|
const host = hostOf(url);
|
|
2054
2261
|
return host ? (0, import_node_path.join)(AUTH_DIR, `${host}.json`) : null;
|
|
2055
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
|
+
}
|
|
2056
2267
|
function findAuthState(url) {
|
|
2057
2268
|
const path = authStatePath(url);
|
|
2058
2269
|
return path && (0, import_node_fs.existsSync)(path) ? path : void 0;
|
|
2059
2270
|
}
|
|
2060
|
-
async function saveAuthState(url, state) {
|
|
2271
|
+
async function saveAuthState(url, state, startUrl) {
|
|
2061
2272
|
const path = authStatePath(url);
|
|
2062
2273
|
if (!path) throw new Error(`Not a valid URL: ${url}`);
|
|
2063
2274
|
await (0, import_promises.mkdir)(AUTH_DIR, { recursive: true });
|
|
2064
2275
|
await (0, import_promises.writeFile)(path, JSON.stringify(state, null, 2));
|
|
2065
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
|
+
}
|
|
2066
2280
|
return path;
|
|
2067
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
|
+
}
|
|
2068
2302
|
async function clearAuthState(url) {
|
|
2069
2303
|
if (url) {
|
|
2070
2304
|
const path = authStatePath(url);
|
|
2071
2305
|
if (!path || !(0, import_node_fs.existsSync)(path)) return [];
|
|
2072
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);
|
|
2073
2309
|
return [hostOf(url)];
|
|
2074
2310
|
}
|
|
2075
2311
|
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2076
2312
|
const files = (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json"));
|
|
2077
2313
|
await Promise.all(files.map((f) => (0, import_promises.unlink)((0, import_node_path.join)(AUTH_DIR, f))));
|
|
2078
|
-
return files.map((f) => f.slice(0, -".json".length));
|
|
2314
|
+
return files.filter((f) => !f.endsWith(".meta.json")).map((f) => f.slice(0, -".json".length));
|
|
2079
2315
|
}
|
|
2080
2316
|
async function listAuthStates() {
|
|
2081
2317
|
if (!(0, import_node_fs.existsSync)(AUTH_DIR)) return [];
|
|
2082
|
-
return (await (0, import_promises.readdir)(AUTH_DIR)).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -".json".length));
|
|
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));
|
|
2083
2319
|
}
|
|
2084
2320
|
|
|
2085
|
-
// src/
|
|
2086
|
-
var
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
// src/logo.ts
|
|
2090
|
-
var import_figlet = __toESM(require("figlet"));
|
|
2091
|
-
var import_gradient_string = __toESM(require("gradient-string"));
|
|
2092
|
-
var DEFAULT_LOGO_CONFIG = {
|
|
2093
|
-
text: "QuickPeek",
|
|
2094
|
-
font: "Slant",
|
|
2095
|
-
horizontalLayout: "default",
|
|
2096
|
-
gradientColors: ["#00ffff", "#00d4ff", "#a855f7", "#ec4899"],
|
|
2097
|
-
suffix: " \u{1F440}"
|
|
2098
|
-
};
|
|
2099
|
-
function renderLogo(config = {}) {
|
|
2100
|
-
const resolved = { ...DEFAULT_LOGO_CONFIG, ...config };
|
|
2101
|
-
const ascii = import_figlet.default.textSync(resolved.text, {
|
|
2102
|
-
font: resolved.font,
|
|
2103
|
-
horizontalLayout: resolved.horizontalLayout
|
|
2104
|
-
});
|
|
2105
|
-
const colored = (0, import_gradient_string.default)(resolved.gradientColors)(ascii);
|
|
2106
|
-
return resolved.suffix ? `${colored}${resolved.suffix}` : colored;
|
|
2107
|
-
}
|
|
2321
|
+
// src/browser/launch.ts
|
|
2322
|
+
var import_promises2 = require("fs/promises");
|
|
2323
|
+
var import_playwright2 = require("playwright");
|
|
2108
2324
|
|
|
2109
|
-
// src/
|
|
2110
|
-
var
|
|
2111
|
-
var
|
|
2112
|
-
var
|
|
2113
|
-
var
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
const config = {
|
|
2122
|
-
lang: langOverride || userConfig.lang || "en",
|
|
2123
|
-
plan: { ...DEFAULT_CONFIG.plan, ...userConfig.plan },
|
|
2124
|
-
video: { ...DEFAULT_CONFIG.video, ...userConfig.video },
|
|
2125
|
-
audio: { ...DEFAULT_CONFIG.audio, ...userConfig.audio },
|
|
2126
|
-
music: userConfig.music,
|
|
2127
|
-
timing: { ...DEFAULT_CONFIG.timing, ...userConfig.timing },
|
|
2128
|
-
output: { ...DEFAULT_CONFIG.output, ...userConfig.output }
|
|
2129
|
-
};
|
|
2130
|
-
return { config, created: false };
|
|
2131
|
-
} catch {
|
|
2132
|
-
}
|
|
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;
|
|
2133
2337
|
}
|
|
2134
|
-
const fullDefaultConfig = {
|
|
2135
|
-
lang: "en",
|
|
2136
|
-
plan: { maxSteps: 8 },
|
|
2137
|
-
video: {
|
|
2138
|
-
width: 1080,
|
|
2139
|
-
height: 1920,
|
|
2140
|
-
fps: 30,
|
|
2141
|
-
quality: 0.5,
|
|
2142
|
-
preset: "fast",
|
|
2143
|
-
cursor: true,
|
|
2144
|
-
highlight: false,
|
|
2145
|
-
fadeIn: 500,
|
|
2146
|
-
// ms
|
|
2147
|
-
fadeOut: 500,
|
|
2148
|
-
// ms
|
|
2149
|
-
contrast: 0.25
|
|
2150
|
-
},
|
|
2151
|
-
audio: {
|
|
2152
|
-
// Voice is derived from lang setting via LANGUAGE_VOICES mapping
|
|
2153
|
-
// To override, use Edge TTS voice name like 'en-GB-SoniaNeural'
|
|
2154
|
-
speed: 1
|
|
2155
|
-
},
|
|
2156
|
-
music: {
|
|
2157
|
-
path: "",
|
|
2158
|
-
volume: 0.3
|
|
2159
|
-
},
|
|
2160
|
-
output: { dir: "./demo" }
|
|
2161
|
-
};
|
|
2162
|
-
await (0, import_promises5.writeFile)(configPath, JSON.stringify(fullDefaultConfig, null, 2));
|
|
2163
|
-
return { config: { ...DEFAULT_CONFIG, lang: langOverride || "en" }, created: true };
|
|
2164
2338
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
const workDir = (0, import_node_path7.join)(process.cwd(), WORK_DIR);
|
|
2168
|
-
if ((0, import_node_fs4.existsSync)(workDir)) {
|
|
2169
|
-
await (0, import_promises5.rm)(workDir, { recursive: true });
|
|
2170
|
-
}
|
|
2171
|
-
await (0, import_promises5.mkdir)(workDir, { recursive: true });
|
|
2172
|
-
return workDir;
|
|
2339
|
+
function tryEnableWSLInterop() {
|
|
2340
|
+
return (0, import_node_fs2.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2173
2341
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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;
|
|
2177
2350
|
try {
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
}
|
|
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, "/");
|
|
2181
2353
|
} catch {
|
|
2182
2354
|
}
|
|
2183
|
-
return
|
|
2355
|
+
return null;
|
|
2184
2356
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
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 {
|
|
2188
2370
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
-webkit-text-stroke: 3px #000000;
|
|
2196
|
-
}
|
|
2197
|
-
.caption .regular {
|
|
2198
|
-
font-size: 56px;
|
|
2199
|
-
color: #FFFFFF;
|
|
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";
|
|
2200
2377
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
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
|
+
});
|
|
2204
2397
|
}
|
|
2205
|
-
|
|
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;
|
|
2719
|
+
}
|
|
2720
|
+
.caption .highlight {
|
|
2721
|
+
font-size: 64px;
|
|
2722
|
+
color: #FFFF00;
|
|
2723
|
+
}
|
|
2724
|
+
`;
|
|
2206
2725
|
function parseCaptionCSS(css) {
|
|
2207
2726
|
const style = JSON.parse(JSON.stringify(DEFAULT_CAPTIONS));
|
|
2208
2727
|
const captionBlock = css.match(/\.caption\s*\{([^}]+)\}/)?.[1] || "";
|
|
@@ -2250,12 +2769,12 @@ function parseCaptionCSS(css) {
|
|
|
2250
2769
|
}
|
|
2251
2770
|
async function loadCaptions() {
|
|
2252
2771
|
const captionsPath = (0, import_node_path7.join)(process.cwd(), "captions.css");
|
|
2253
|
-
if (!(0,
|
|
2254
|
-
await (0,
|
|
2772
|
+
if (!(0, import_node_fs6.existsSync)(captionsPath)) {
|
|
2773
|
+
await (0, import_promises7.writeFile)(captionsPath, DEFAULT_CAPTIONS_CSS);
|
|
2255
2774
|
return DEFAULT_CAPTIONS;
|
|
2256
2775
|
}
|
|
2257
2776
|
try {
|
|
2258
|
-
const css = await (0,
|
|
2777
|
+
const css = await (0, import_promises7.readFile)(captionsPath, "utf-8");
|
|
2259
2778
|
return parseCaptionCSS(css);
|
|
2260
2779
|
} catch {
|
|
2261
2780
|
return DEFAULT_CAPTIONS;
|
|
@@ -2266,8 +2785,8 @@ async function savePlan(plan, planPath) {
|
|
|
2266
2785
|
...plan,
|
|
2267
2786
|
steps: plan.steps.map(({ duration, ...step }) => step)
|
|
2268
2787
|
};
|
|
2269
|
-
await (0,
|
|
2270
|
-
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));
|
|
2271
2790
|
}
|
|
2272
2791
|
|
|
2273
2792
|
// src/help.ts
|
|
@@ -2346,6 +2865,7 @@ async function showHelp() {
|
|
|
2346
2865
|
[`${cmd("qp login")}`, `qp login`, "Sign in with your Google account"],
|
|
2347
2866
|
[`${cmd("qp logout")}`, `qp logout`, "Sign out"],
|
|
2348
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"],
|
|
2349
2869
|
[`${cmd("qp auth clear")} ${arg("[url]")}`, `qp auth clear [url]`, "Forget saved sessions"],
|
|
2350
2870
|
[`${cmd("qp init")}`, `qp init`, "Create config file"],
|
|
2351
2871
|
[`${cmd("qp config")} ${arg("<key>")} ${arg("<value>")}`, `qp config <key> <value>`, "Update a config option"]
|
|
@@ -2400,6 +2920,9 @@ var autoYes = false;
|
|
|
2400
2920
|
function setAutoYes(value) {
|
|
2401
2921
|
autoYes = value;
|
|
2402
2922
|
}
|
|
2923
|
+
function isAutoYes() {
|
|
2924
|
+
return autoYes;
|
|
2925
|
+
}
|
|
2403
2926
|
function task(text) {
|
|
2404
2927
|
return (0, import_ora.default)({ text, prefixText: INDENT, spinner: "dots" }).start();
|
|
2405
2928
|
}
|
|
@@ -2430,11 +2953,20 @@ function promptInput(prompt2) {
|
|
|
2430
2953
|
});
|
|
2431
2954
|
});
|
|
2432
2955
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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
|
+
}
|
|
2965
|
+
var _isWSL = process.platform === "linux" && process.env.WSL_DISTRO_NAME;
|
|
2966
|
+
function formatClickablePath(filePath) {
|
|
2967
|
+
const absolutePath = (0, import_node_path8.resolve)(filePath);
|
|
2968
|
+
if (_isWSL || process.platform !== "win32") {
|
|
2969
|
+
return absolutePath;
|
|
2438
2970
|
}
|
|
2439
2971
|
let formatted = absolutePath;
|
|
2440
2972
|
formatted = formatted.replace(/\//g, "\\");
|
|
@@ -2450,15 +2982,6 @@ function formatDuration(ms) {
|
|
|
2450
2982
|
}
|
|
2451
2983
|
|
|
2452
2984
|
// src/commands/auth-cmd.ts
|
|
2453
|
-
function waitForEnter(prompt2) {
|
|
2454
|
-
return new Promise((resolve9) => {
|
|
2455
|
-
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2456
|
-
rl.question(prompt2, () => {
|
|
2457
|
-
rl.close();
|
|
2458
|
-
resolve9();
|
|
2459
|
-
});
|
|
2460
|
-
});
|
|
2461
|
-
}
|
|
2462
2985
|
async function authCommand(url) {
|
|
2463
2986
|
console.log();
|
|
2464
2987
|
showLogo();
|
|
@@ -2466,9 +2989,10 @@ async function authCommand(url) {
|
|
|
2466
2989
|
console.log(import_chalk3.default.red(` Not a valid URL: ${url}`));
|
|
2467
2990
|
return;
|
|
2468
2991
|
}
|
|
2469
|
-
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."));
|
|
2470
2994
|
console.log();
|
|
2471
|
-
const browser = await
|
|
2995
|
+
const browser = await launchSignInBrowser();
|
|
2472
2996
|
const context = await browser.newContext();
|
|
2473
2997
|
const page = await context.newPage();
|
|
2474
2998
|
try {
|
|
@@ -2478,11 +3002,15 @@ async function authCommand(url) {
|
|
|
2478
3002
|
await waitForEnter(import_chalk3.default.cyan(" Press Enter when you are signed in... "));
|
|
2479
3003
|
try {
|
|
2480
3004
|
const state = await context.storageState();
|
|
2481
|
-
const
|
|
3005
|
+
const startUrl = captureStartUrl(context, url);
|
|
3006
|
+
const path = await saveAuthState(url, state, startUrl);
|
|
2482
3007
|
console.log();
|
|
2483
3008
|
console.log(import_chalk3.default.green(` \u2713 Session saved for ${new URL(url).host}`));
|
|
2484
3009
|
console.log(import_chalk3.default.dim(` ${formatClickablePath(path)}`));
|
|
2485
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
|
+
}
|
|
2486
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."));
|
|
2487
3015
|
} finally {
|
|
2488
3016
|
await browser.close().catch(() => {
|
|
@@ -2524,17 +3052,17 @@ var import_chalk11 = __toESM(require("chalk"));
|
|
|
2524
3052
|
init_lib();
|
|
2525
3053
|
|
|
2526
3054
|
// src/ai/client.ts
|
|
2527
|
-
var
|
|
3055
|
+
var import_node_readline2 = require("readline");
|
|
2528
3056
|
var import_chalk5 = __toESM(require("chalk"));
|
|
2529
3057
|
init_lib();
|
|
2530
3058
|
|
|
2531
3059
|
// src/utils/browser-auth.ts
|
|
2532
|
-
var
|
|
3060
|
+
var import_node_http2 = require("http");
|
|
2533
3061
|
var import_node_url = require("url");
|
|
2534
3062
|
var import_chalk4 = __toESM(require("chalk"));
|
|
2535
3063
|
|
|
2536
3064
|
// src/lib/browser-open.ts
|
|
2537
|
-
var
|
|
3065
|
+
var import_node_child_process4 = require("child_process");
|
|
2538
3066
|
function editorBaseUrl() {
|
|
2539
3067
|
return (process.env.QUICKPEEK_API_BASE || "https://quickpeek.co").replace(/\/$/, "");
|
|
2540
3068
|
}
|
|
@@ -2542,7 +3070,7 @@ function openInBrowser(url) {
|
|
|
2542
3070
|
const isWSL2 = process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME);
|
|
2543
3071
|
const command = isWSL2 ? `cmd.exe /c start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
2544
3072
|
return new Promise((resolve9) => {
|
|
2545
|
-
(0,
|
|
3073
|
+
(0, import_node_child_process4.exec)(command, () => resolve9());
|
|
2546
3074
|
});
|
|
2547
3075
|
}
|
|
2548
3076
|
|
|
@@ -2558,7 +3086,7 @@ async function browserLogin() {
|
|
|
2558
3086
|
server?.close();
|
|
2559
3087
|
resolve9(email);
|
|
2560
3088
|
};
|
|
2561
|
-
server = (0,
|
|
3089
|
+
server = (0, import_node_http2.createServer)((req, res) => {
|
|
2562
3090
|
const url = new import_node_url.URL(req.url || "/", `http://localhost`);
|
|
2563
3091
|
const path = url.pathname;
|
|
2564
3092
|
if (path === "/callback" || path === "/") {
|
|
@@ -2612,13 +3140,132 @@ async function browserLogin() {
|
|
|
2612
3140
|
});
|
|
2613
3141
|
}
|
|
2614
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
|
+
|
|
2615
3234
|
// src/ai/client.ts
|
|
2616
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
|
+
}
|
|
2617
3264
|
function getLastRateLimitInfo() {
|
|
2618
3265
|
return lastRateLimitInfo;
|
|
2619
3266
|
}
|
|
2620
3267
|
function prompt(question) {
|
|
2621
|
-
const rl = (0,
|
|
3268
|
+
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2622
3269
|
return new Promise((resolve9) => {
|
|
2623
3270
|
rl.question(question, (answer) => {
|
|
2624
3271
|
rl.close();
|
|
@@ -2661,15 +3308,33 @@ async function setupUserEmail() {
|
|
|
2661
3308
|
console.log();
|
|
2662
3309
|
return email;
|
|
2663
3310
|
}
|
|
2664
|
-
async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "free") {
|
|
2665
|
-
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);
|
|
2666
3330
|
if (!result.success) {
|
|
2667
|
-
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));
|
|
2668
3333
|
if (result.isRateLimit) {
|
|
2669
3334
|
console.log();
|
|
2670
3335
|
console.log(import_chalk5.default.red(" \u26A0 Rate Limit Reached (after 3 retries)"));
|
|
2671
3336
|
console.log();
|
|
2672
|
-
console.log(import_chalk5.default.dim(
|
|
3337
|
+
console.log(import_chalk5.default.dim(` ${result.error}`));
|
|
2673
3338
|
console.log(import_chalk5.default.dim(" Options:"));
|
|
2674
3339
|
if (result.retryAfterSeconds) {
|
|
2675
3340
|
const minutes = Math.ceil(result.retryAfterSeconds / 60);
|
|
@@ -2684,17 +3349,31 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
|
|
|
2684
3349
|
}
|
|
2685
3350
|
if (result.isAuthError) {
|
|
2686
3351
|
console.log();
|
|
2687
|
-
console.log(import_chalk5.default.red(
|
|
3352
|
+
console.log(import_chalk5.default.red(` \u26A0 Authentication Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
2688
3353
|
console.log();
|
|
2689
|
-
|
|
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)}`));
|
|
2690
3359
|
console.log();
|
|
2691
3360
|
process.exit(1);
|
|
2692
3361
|
}
|
|
2693
3362
|
console.log();
|
|
2694
|
-
console.log(import_chalk5.default.red(
|
|
3363
|
+
console.log(import_chalk5.default.red(` \u26A0 AI Error${result.status ? ` (HTTP ${result.status})` : ""}`));
|
|
3364
|
+
console.log();
|
|
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
|
+
}
|
|
2695
3374
|
console.log();
|
|
2696
|
-
|
|
2697
|
-
console.log(import_chalk5.default.dim(` ${
|
|
3375
|
+
console.log(import_chalk5.default.dim(` ${diagnose(result.status, result.host)}`));
|
|
3376
|
+
console.log(import_chalk5.default.dim(` Full response body: ${logFilePath}`));
|
|
2698
3377
|
console.log();
|
|
2699
3378
|
process.exit(1);
|
|
2700
3379
|
}
|
|
@@ -2702,7 +3381,48 @@ async function callGroq(systemPrompt, userPrompt, logError2, userEmail, tier = "
|
|
|
2702
3381
|
return result.content;
|
|
2703
3382
|
}
|
|
2704
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
|
+
|
|
2705
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
|
+
}
|
|
2706
3426
|
async function generateCaptionsForSteps(steps, url, title, lang, logError2, userEmail, pageContext, tier = "free") {
|
|
2707
3427
|
const langName = getLanguageName(lang);
|
|
2708
3428
|
const stepList = steps.map((s, i) => {
|
|
@@ -2728,33 +3448,34 @@ Steps to caption:
|
|
|
2728
3448
|
${stepList}
|
|
2729
3449
|
|
|
2730
3450
|
Return exactly ${steps.length} captions as a JSON array.`;
|
|
2731
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
2732
|
-
const
|
|
2733
|
-
if (!
|
|
2734
|
-
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;
|
|
2735
3455
|
return steps.map((s, i) => ({
|
|
2736
3456
|
...s,
|
|
2737
3457
|
id: i + 1,
|
|
2738
3458
|
caption: toTitleCase(captions[i] ?? `Step ${i + 1}.`)
|
|
2739
3459
|
}));
|
|
2740
3460
|
}
|
|
2741
|
-
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) {
|
|
2742
3462
|
const systemPrompt = `You map narration sentences to web UI actions.
|
|
2743
|
-
Given page elements and a script, return
|
|
3463
|
+
Given page elements and a numbered script, return one action per script line.
|
|
2744
3464
|
|
|
2745
3465
|
RULES:
|
|
2746
|
-
- Return ONLY valid JSON
|
|
2747
|
-
-
|
|
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
|
|
2748
3468
|
- Use page elements to pick realistic actions/targets
|
|
2749
|
-
- If a
|
|
3469
|
+
- If a line doesn't match a specific UI action, use { "action": "wait" }
|
|
3470
|
+
- Keep every target and value short
|
|
2750
3471
|
|
|
2751
3472
|
ACTION FORMATS:
|
|
2752
|
-
\u2022 click: { "action": "click", "target": "button text" }
|
|
2753
|
-
\u2022 type: { "action": "type", "target": "input selector", "value": "text
|
|
2754
|
-
\u2022 scroll: { "action": "scroll", "value": "up|down" }
|
|
2755
|
-
\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" }
|
|
2756
3477
|
|
|
2757
|
-
OUTPUT:
|
|
3478
|
+
OUTPUT: {"description":"one short sentence","steps":[ ... ]}`;
|
|
2758
3479
|
const scriptLines = captions.map((c, i) => `${i + 1}. "${c}"`).join("\n");
|
|
2759
3480
|
const userMessage = `URL: ${url}
|
|
2760
3481
|
App: ${title}
|
|
@@ -2762,22 +3483,46 @@ App: ${title}
|
|
|
2762
3483
|
Available page elements:
|
|
2763
3484
|
${pageElementsText}
|
|
2764
3485
|
|
|
2765
|
-
Narration script (map each line to a UI action
|
|
3486
|
+
Narration script (map each line to a UI action):
|
|
2766
3487
|
${scriptLines}
|
|
2767
3488
|
|
|
2768
|
-
Return
|
|
2769
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
+
};
|
|
2781
3526
|
}
|
|
2782
3527
|
|
|
2783
3528
|
// src/browser/events-to-plan.ts
|
|
@@ -2845,191 +3590,48 @@ function eventsToSteps(events, url) {
|
|
|
2845
3590
|
continue;
|
|
2846
3591
|
}
|
|
2847
3592
|
if (event.type === "click") {
|
|
2848
|
-
const selectorRaw = !isGenericTag(event.selector) ? event.selector : event.target;
|
|
2849
|
-
if (isGenericTag(selectorRaw)) continue;
|
|
2850
|
-
const selector = selectorRaw;
|
|
2851
|
-
const timeDiff = event.timestamp - prevClickTime;
|
|
2852
|
-
if (selector === prevClickSelector && timeDiff < DOUBLE_CLICK_THRESHOLD_MS && steps.length > 0) {
|
|
2853
|
-
const last = steps[steps.length - 1];
|
|
2854
|
-
if (last.action === "click") {
|
|
2855
|
-
last.action = "click-2";
|
|
2856
|
-
prevClickSelector = void 0;
|
|
2857
|
-
continue;
|
|
2858
|
-
}
|
|
2859
|
-
}
|
|
2860
|
-
steps.push({ id: idCounter++, action: "click", target: selector });
|
|
2861
|
-
prevClickSelector = selector;
|
|
2862
|
-
prevClickTime = event.timestamp;
|
|
2863
|
-
continue;
|
|
2864
|
-
}
|
|
2865
|
-
if (event.type === "scroll") {
|
|
2866
|
-
const dir = event.scrollDir ?? "down";
|
|
2867
|
-
if (steps.length > 0) {
|
|
2868
|
-
const last = steps[steps.length - 1];
|
|
2869
|
-
if (last.action === "scroll" && last.value === dir) continue;
|
|
2870
|
-
}
|
|
2871
|
-
steps.push({ id: idCounter++, action: "scroll", value: dir });
|
|
2872
|
-
prevClickSelector = void 0;
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
return steps;
|
|
2876
|
-
}
|
|
2877
|
-
function normalizeUrl2(url) {
|
|
2878
|
-
try {
|
|
2879
|
-
const u = new URL(url);
|
|
2880
|
-
return u.origin + u.pathname.replace(/\/$/, "");
|
|
2881
|
-
} catch {
|
|
2882
|
-
return url;
|
|
2883
|
-
}
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
// src/capture.ts
|
|
2887
|
-
var import_node_child_process5 = require("child_process");
|
|
2888
|
-
var import_node_http3 = require("http");
|
|
2889
|
-
var import_node_readline4 = require("readline");
|
|
2890
|
-
var import_playwright4 = require("playwright");
|
|
2891
|
-
|
|
2892
|
-
// src/browser/wsl.ts
|
|
2893
|
-
var import_node_child_process4 = require("child_process");
|
|
2894
|
-
var import_node_fs5 = require("fs");
|
|
2895
|
-
var import_node_http2 = require("http");
|
|
2896
|
-
var import_playwright3 = require("playwright");
|
|
2897
|
-
function isWSL() {
|
|
2898
|
-
if (process.platform !== "linux") return false;
|
|
2899
|
-
try {
|
|
2900
|
-
const r = (0, import_node_child_process4.execSync)("uname -r", { encoding: "utf-8" }).toLowerCase();
|
|
2901
|
-
return r.includes("microsoft") || r.includes("wsl");
|
|
2902
|
-
} catch {
|
|
2903
|
-
return false;
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
function tryEnableWSLInterop() {
|
|
2907
|
-
return (0, import_node_fs5.existsSync)("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
2908
|
-
}
|
|
2909
|
-
function wslToWindowsPath(wslPath) {
|
|
2910
|
-
if (!wslPath) return wslPath;
|
|
2911
|
-
const m = wslPath.match(/^\/mnt\/([a-z])\/(.*)/);
|
|
2912
|
-
if (!m) return wslPath;
|
|
2913
|
-
return `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, "\\")}`;
|
|
2914
|
-
}
|
|
2915
|
-
function findExe(whereExeQuery, ...hardcoded) {
|
|
2916
|
-
for (const p of hardcoded) if ((0, import_node_fs5.existsSync)(p)) return p;
|
|
2917
|
-
try {
|
|
2918
|
-
const line = (0, import_node_child_process4.execSync)(`where.exe ${whereExeQuery}`, { encoding: "utf-8", timeout: 3e3 }).split("\n")[0]?.trim();
|
|
2919
|
-
if (line) return line.replace(/^([A-Za-z]):\\/, (_, d) => `/mnt/${d.toLowerCase()}/`).replace(/\\/g, "/");
|
|
2920
|
-
} catch {
|
|
2921
|
-
}
|
|
2922
|
-
return null;
|
|
2923
|
-
}
|
|
2924
|
-
var findWindowsChrome = () => findExe(
|
|
2925
|
-
"chrome.exe",
|
|
2926
|
-
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2927
|
-
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
|
|
2928
|
-
);
|
|
2929
|
-
var findWindowsNode = () => findExe("node.exe");
|
|
2930
|
-
function getWindowsHostIP() {
|
|
2931
|
-
try {
|
|
2932
|
-
const m = (0, import_node_child_process4.execSync)("ip route show default", { encoding: "utf-8", timeout: 2e3 }).match(
|
|
2933
|
-
/default via (\d+\.\d+\.\d+\.\d+)/
|
|
2934
|
-
);
|
|
2935
|
-
if (m?.[1]) return m[1];
|
|
2936
|
-
} catch {
|
|
2937
|
-
}
|
|
2938
|
-
try {
|
|
2939
|
-
const m = (0, import_node_fs5.readFileSync)("/etc/resolv.conf", "utf-8").match(/nameserver\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
2940
|
-
if (m?.[1]) return m[1];
|
|
2941
|
-
} catch {
|
|
2942
|
-
}
|
|
2943
|
-
return "127.0.0.1";
|
|
2944
|
-
}
|
|
2945
|
-
function startRelay(nodePath, windowsHost, targetPort) {
|
|
2946
|
-
const script = `var n=require('net'),s=n.createServer(function(c){var r=n.createConnection(${targetPort},'127.0.0.1');c.pipe(r);r.pipe(c);r.on('error',function(){c.destroy()});c.on('error',function(){r.destroy()});});s.listen(0,'${windowsHost}',function(){process.stdout.write('PORT:'+s.address().port+'\\n')});`;
|
|
2947
|
-
return new Promise((resolve9, reject) => {
|
|
2948
|
-
const proc = (0, import_node_child_process4.spawn)(nodePath, ["-e", script], { stdio: ["ignore", "pipe", "ignore"], detached: true });
|
|
2949
|
-
const timer = setTimeout(() => reject(new Error("relay startup timed out")), 6e3);
|
|
2950
|
-
let buf = "";
|
|
2951
|
-
proc.stdout.on("data", (d) => {
|
|
2952
|
-
buf += d.toString();
|
|
2953
|
-
const m = buf.match(/PORT:(\d+)/);
|
|
2954
|
-
if (m) {
|
|
2955
|
-
clearTimeout(timer);
|
|
2956
|
-
resolve9({ proc, port: +m[1] });
|
|
2957
|
-
}
|
|
2958
|
-
});
|
|
2959
|
-
proc.on("error", (e) => {
|
|
2960
|
-
clearTimeout(timer);
|
|
2961
|
-
reject(e);
|
|
2962
|
-
});
|
|
2963
|
-
});
|
|
2964
|
-
}
|
|
2965
|
-
function waitForChrome(host, port) {
|
|
2966
|
-
return new Promise((resolve9, reject) => {
|
|
2967
|
-
const deadline = Date.now() + 2e4;
|
|
2968
|
-
const poll = () => {
|
|
2969
|
-
if (Date.now() > deadline) {
|
|
2970
|
-
reject(new Error("Chrome debug port did not open within 20 s"));
|
|
2971
|
-
return;
|
|
2972
|
-
}
|
|
2973
|
-
const req = (0, import_node_http2.request)({ host, port, path: "/json/version", method: "GET" }, (res) => {
|
|
2974
|
-
res.resume();
|
|
2975
|
-
resolve9();
|
|
2976
|
-
});
|
|
2977
|
-
req.on("error", () => setTimeout(poll, 600));
|
|
2978
|
-
req.setTimeout(1e3, () => {
|
|
2979
|
-
req.destroy();
|
|
2980
|
-
setTimeout(poll, 600);
|
|
2981
|
-
});
|
|
2982
|
-
req.end();
|
|
2983
|
-
};
|
|
2984
|
-
poll();
|
|
2985
|
-
});
|
|
2986
|
-
}
|
|
2987
|
-
async function launchWindowsChrome(extensionPath, userDataDir, options = {}) {
|
|
2988
|
-
const debugPort = options.debugPort ?? 9222;
|
|
2989
|
-
const chromePath = findWindowsChrome();
|
|
2990
|
-
if (!chromePath) throw new Error("Windows Chrome not found. Install Google Chrome for Windows.");
|
|
2991
|
-
const nodePath = findWindowsNode();
|
|
2992
|
-
if (!nodePath) throw new Error("Windows Node.js not found. Install Node.js for Windows.");
|
|
2993
|
-
const winUserData = wslToWindowsPath(userDataDir);
|
|
2994
|
-
const winExtPath = extensionPath ? wslToWindowsPath(extensionPath) : "";
|
|
2995
|
-
const chromeProc = (0, import_node_child_process4.spawn)(
|
|
2996
|
-
chromePath,
|
|
2997
|
-
[
|
|
2998
|
-
`--remote-debugging-port=${debugPort}`,
|
|
2999
|
-
`--user-data-dir=${winUserData}`,
|
|
3000
|
-
...winExtPath ? [`--disable-extensions-except=${winExtPath}`, `--load-extension=${winExtPath}`] : [],
|
|
3001
|
-
"--no-first-run",
|
|
3002
|
-
"--no-default-browser-check",
|
|
3003
|
-
"--disable-background-networking",
|
|
3004
|
-
"--disable-sync",
|
|
3005
|
-
"--disable-features=TranslateUI",
|
|
3006
|
-
options.startMaximized ? "--start-maximized" : "--start-minimized"
|
|
3007
|
-
],
|
|
3008
|
-
{ stdio: "ignore", detached: true }
|
|
3009
|
-
);
|
|
3010
|
-
chromeProc.unref();
|
|
3011
|
-
const windowsHost = getWindowsHostIP();
|
|
3012
|
-
const { proc: relay, port: proxyPort } = await startRelay(nodePath, windowsHost, debugPort);
|
|
3013
|
-
try {
|
|
3014
|
-
await waitForChrome(windowsHost, proxyPort);
|
|
3015
|
-
const browser = await import_playwright3.chromium.connectOverCDP(`http://${windowsHost}:${proxyPort}`, { timeout: 5e3 });
|
|
3016
|
-
return {
|
|
3017
|
-
browser,
|
|
3018
|
-
cleanup: async () => {
|
|
3019
|
-
try {
|
|
3020
|
-
await browser.close();
|
|
3021
|
-
} catch {
|
|
3593
|
+
const selectorRaw = !isGenericTag(event.selector) ? event.selector : event.target;
|
|
3594
|
+
if (isGenericTag(selectorRaw)) continue;
|
|
3595
|
+
const selector = selectorRaw;
|
|
3596
|
+
const timeDiff = event.timestamp - prevClickTime;
|
|
3597
|
+
if (selector === prevClickSelector && timeDiff < DOUBLE_CLICK_THRESHOLD_MS && steps.length > 0) {
|
|
3598
|
+
const last = steps[steps.length - 1];
|
|
3599
|
+
if (last.action === "click") {
|
|
3600
|
+
last.action = "click-2";
|
|
3601
|
+
prevClickSelector = void 0;
|
|
3602
|
+
continue;
|
|
3022
3603
|
}
|
|
3023
|
-
relay.kill();
|
|
3024
3604
|
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3605
|
+
steps.push({ id: idCounter++, action: "click", target: selector });
|
|
3606
|
+
prevClickSelector = selector;
|
|
3607
|
+
prevClickTime = event.timestamp;
|
|
3608
|
+
continue;
|
|
3609
|
+
}
|
|
3610
|
+
if (event.type === "scroll") {
|
|
3611
|
+
const dir = event.scrollDir ?? "down";
|
|
3612
|
+
if (steps.length > 0) {
|
|
3613
|
+
const last = steps[steps.length - 1];
|
|
3614
|
+
if (last.action === "scroll" && last.value === dir) continue;
|
|
3615
|
+
}
|
|
3616
|
+
steps.push({ id: idCounter++, action: "scroll", value: dir });
|
|
3617
|
+
prevClickSelector = void 0;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return steps;
|
|
3621
|
+
}
|
|
3622
|
+
function normalizeUrl2(url) {
|
|
3623
|
+
try {
|
|
3624
|
+
const u = new URL(url);
|
|
3625
|
+
return u.origin + u.pathname.replace(/\/$/, "");
|
|
3626
|
+
} catch {
|
|
3627
|
+
return url;
|
|
3029
3628
|
}
|
|
3030
3629
|
}
|
|
3031
3630
|
|
|
3032
3631
|
// src/capture.ts
|
|
3632
|
+
var import_node_child_process5 = require("child_process");
|
|
3633
|
+
var import_node_http3 = require("http");
|
|
3634
|
+
var import_playwright4 = require("playwright");
|
|
3033
3635
|
var EVENT_TRACKER_SCRIPT = `
|
|
3034
3636
|
(function () {
|
|
3035
3637
|
if (window.__qpTracking) return;
|
|
@@ -3141,16 +3743,6 @@ var EVENT_TRACKER_SCRIPT = `
|
|
|
3141
3743
|
}, { passive: true, capture: true });
|
|
3142
3744
|
}());
|
|
3143
3745
|
`;
|
|
3144
|
-
function waitForEnter2() {
|
|
3145
|
-
return new Promise((resolve9) => {
|
|
3146
|
-
const rl = (0, import_node_readline4.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3147
|
-
process.stdout.write(" Press Enter when done...\n");
|
|
3148
|
-
rl.once("line", () => {
|
|
3149
|
-
rl.close();
|
|
3150
|
-
resolve9();
|
|
3151
|
-
});
|
|
3152
|
-
});
|
|
3153
|
-
}
|
|
3154
3746
|
async function launchCaptureChrome() {
|
|
3155
3747
|
const debugPort = 9222;
|
|
3156
3748
|
const chromePath = findWindowsChrome();
|
|
@@ -3249,7 +3841,7 @@ async function captureUserSession(url, _workDir) {
|
|
|
3249
3841
|
return [title, parts.join(" | ")];
|
|
3250
3842
|
}).catch(() => ["", ""]);
|
|
3251
3843
|
startTime = Date.now();
|
|
3252
|
-
await
|
|
3844
|
+
await waitForEnter();
|
|
3253
3845
|
const durationMs = Date.now() - startTime;
|
|
3254
3846
|
if (relay) {
|
|
3255
3847
|
try {
|
|
@@ -3264,83 +3856,25 @@ async function captureUserSession(url, _workDir) {
|
|
|
3264
3856
|
return { events, durationMs, pageTitle, pageContext };
|
|
3265
3857
|
}
|
|
3266
3858
|
|
|
3267
|
-
// src/utils/logging.ts
|
|
3268
|
-
var import_promises6 = require("fs/promises");
|
|
3269
|
-
var import_node_path9 = require("path");
|
|
3270
|
-
var LOG_FILE_NAME = "quickpeek.log";
|
|
3271
|
-
var logFileInitialized = false;
|
|
3272
|
-
var logFilePath = LOG_FILE_NAME;
|
|
3273
|
-
async function writeLogEntry(entry) {
|
|
3274
|
-
try {
|
|
3275
|
-
if (!logFileInitialized) {
|
|
3276
|
-
await (0, import_promises6.writeFile)(logFilePath, entry);
|
|
3277
|
-
logFileInitialized = true;
|
|
3278
|
-
} else {
|
|
3279
|
-
await (0, import_promises6.appendFile)(logFilePath, entry);
|
|
3280
|
-
}
|
|
3281
|
-
} catch {
|
|
3282
|
-
}
|
|
3283
|
-
}
|
|
3284
|
-
async function setLogFilePath(outputDir) {
|
|
3285
|
-
logFilePath = (0, import_node_path9.join)(outputDir, LOG_FILE_NAME);
|
|
3286
|
-
const header2 = `QuickPeek Log - ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3287
|
-
${"=".repeat(60)}
|
|
3288
|
-
|
|
3289
|
-
`;
|
|
3290
|
-
try {
|
|
3291
|
-
await (0, import_promises6.writeFile)(logFilePath, header2);
|
|
3292
|
-
logFileInitialized = true;
|
|
3293
|
-
} catch {
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
async function logInfo(context, message) {
|
|
3297
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [INFO] [${context}] ${message}
|
|
3298
|
-
`);
|
|
3299
|
-
}
|
|
3300
|
-
async function logDebug(context, message) {
|
|
3301
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] [${context}] ${message}
|
|
3302
|
-
`);
|
|
3303
|
-
}
|
|
3304
|
-
async function logError(context, error) {
|
|
3305
|
-
let errorMsg;
|
|
3306
|
-
if (error instanceof Error) {
|
|
3307
|
-
errorMsg = `${error.message}
|
|
3308
|
-
${error.stack}`;
|
|
3309
|
-
} else if (typeof error === "object" && error !== null) {
|
|
3310
|
-
errorMsg = JSON.stringify(error, null, 2);
|
|
3311
|
-
} else {
|
|
3312
|
-
errorMsg = String(error);
|
|
3313
|
-
}
|
|
3314
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [ERROR] [${context}]
|
|
3315
|
-
${errorMsg}
|
|
3316
|
-
|
|
3317
|
-
`);
|
|
3318
|
-
}
|
|
3319
|
-
async function logStep(stepNum, action, details) {
|
|
3320
|
-
const detailsStr = Object.entries(details).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(", ");
|
|
3321
|
-
await writeLogEntry(`[${(/* @__PURE__ */ new Date()).toISOString()}] [STEP ${stepNum}] ${action} | ${detailsStr}
|
|
3322
|
-
`);
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
3859
|
// src/commands/phases/video-phase.ts
|
|
3326
|
-
var
|
|
3327
|
-
var
|
|
3860
|
+
var import_node_fs13 = require("fs");
|
|
3861
|
+
var import_promises14 = require("fs/promises");
|
|
3328
3862
|
var import_node_path18 = require("path");
|
|
3329
3863
|
init_lib();
|
|
3330
3864
|
|
|
3331
3865
|
// src/record.ts
|
|
3332
|
-
var
|
|
3333
|
-
var
|
|
3866
|
+
var import_node_fs11 = require("fs");
|
|
3867
|
+
var import_promises11 = require("fs/promises");
|
|
3334
3868
|
var import_node_path15 = require("path");
|
|
3335
3869
|
var import_chalk6 = __toESM(require("chalk"));
|
|
3336
3870
|
|
|
3337
3871
|
// src/browser/actions.ts
|
|
3338
|
-
var
|
|
3339
|
-
var
|
|
3872
|
+
var import_node_fs8 = require("fs");
|
|
3873
|
+
var import_promises9 = require("fs/promises");
|
|
3340
3874
|
var import_node_path11 = require("path");
|
|
3341
3875
|
|
|
3342
3876
|
// src/browser/effects.ts
|
|
3343
|
-
var
|
|
3877
|
+
var import_node_fs7 = require("fs");
|
|
3344
3878
|
var import_node_path10 = require("path");
|
|
3345
3879
|
init_lib();
|
|
3346
3880
|
function loadHighlightCSS() {
|
|
@@ -3351,7 +3885,7 @@ function loadHighlightCSS() {
|
|
|
3351
3885
|
// src/browser/ → src/
|
|
3352
3886
|
];
|
|
3353
3887
|
for (const p of candidates) {
|
|
3354
|
-
if ((0,
|
|
3888
|
+
if ((0, import_node_fs7.existsSync)(p)) return (0, import_node_fs7.readFileSync)(p, "utf-8");
|
|
3355
3889
|
}
|
|
3356
3890
|
return "";
|
|
3357
3891
|
}
|
|
@@ -3551,6 +4085,10 @@ async function resolveElement(page, target) {
|
|
|
3551
4085
|
await locator.waitFor({ state: "visible", timeout });
|
|
3552
4086
|
return await locator.elementHandle({ timeout: 1e3 });
|
|
3553
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
|
+
}
|
|
3554
4092
|
return await page.waitForSelector(target, { state: "visible", timeout });
|
|
3555
4093
|
} catch {
|
|
3556
4094
|
return null;
|
|
@@ -3700,7 +4238,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3700
4238
|
if (!isPlaceholder) {
|
|
3701
4239
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
3702
4240
|
const filePath = (0, import_node_path11.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path11.resolve)(baseDir, normalizedPath);
|
|
3703
|
-
if ((0,
|
|
4241
|
+
if ((0, import_node_fs8.existsSync)(filePath)) {
|
|
3704
4242
|
await fileInput.setInputFiles(filePath);
|
|
3705
4243
|
await page.waitForTimeout(2e3);
|
|
3706
4244
|
break;
|
|
@@ -3817,7 +4355,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3817
4355
|
break;
|
|
3818
4356
|
case "image": {
|
|
3819
4357
|
const imagePath = step.value ? (0, import_node_path11.isAbsolute)(step.value) ? step.value : (0, import_node_path11.resolve)(baseDir, step.value) : null;
|
|
3820
|
-
if (imagePath && (0,
|
|
4358
|
+
if (imagePath && (0, import_node_fs8.existsSync)(imagePath)) {
|
|
3821
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%;";
|
|
3822
4360
|
if (await showImagePage(page, imagePath, imgStyle, config, cursorInjected)) injected = true;
|
|
3823
4361
|
}
|
|
@@ -3847,7 +4385,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3847
4385
|
await pendingDownload;
|
|
3848
4386
|
pendingDownload = null;
|
|
3849
4387
|
}
|
|
3850
|
-
if (lastDownloadPath && (0,
|
|
4388
|
+
if (lastDownloadPath && (0, import_node_fs8.existsSync)(lastDownloadPath)) {
|
|
3851
4389
|
const imgStyle = step.target === "stretch" ? "width: 100%; height: 100%; object-fit: contain;" : "max-width: 90%; max-height: 90%; object-fit: contain;";
|
|
3852
4390
|
if (await showImagePage(page, lastDownloadPath, imgStyle, config, cursorInjected)) injected = true;
|
|
3853
4391
|
}
|
|
@@ -3858,7 +4396,7 @@ async function executeStep(page, step, plan, config, cursorInjected, baseDir) {
|
|
|
3858
4396
|
async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
3859
4397
|
const { width, height } = config.video;
|
|
3860
4398
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
|
3861
|
-
const fileBuffer = await (0,
|
|
4399
|
+
const fileBuffer = await (0, import_promises9.readFile)(filePath);
|
|
3862
4400
|
const base64 = fileBuffer.toString("base64");
|
|
3863
4401
|
const mimeTypes = {
|
|
3864
4402
|
png: "image/png",
|
|
@@ -3887,64 +4425,6 @@ async function showImagePage(page, filePath, imgStyle, config, cursorInjected) {
|
|
|
3887
4425
|
return false;
|
|
3888
4426
|
}
|
|
3889
4427
|
|
|
3890
|
-
// src/browser/launch.ts
|
|
3891
|
-
var import_promises8 = require("fs/promises");
|
|
3892
|
-
var import_playwright5 = require("playwright");
|
|
3893
|
-
async function launchBrowserContext(config, userDataDir, recordVideo, storageStatePath) {
|
|
3894
|
-
const extensionPath = config.browser?.extension;
|
|
3895
|
-
const viewport = { width: config.video.width, height: config.video.height };
|
|
3896
|
-
const videoOpts = recordVideo ? { recordVideo } : {};
|
|
3897
|
-
const authOpts = storageStatePath ? { storageState: storageStatePath } : {};
|
|
3898
|
-
let context;
|
|
3899
|
-
let browser = null;
|
|
3900
|
-
let chromeSession = null;
|
|
3901
|
-
if (extensionPath) {
|
|
3902
|
-
await (0, import_promises8.mkdir)(userDataDir, { recursive: true });
|
|
3903
|
-
const persistentArgs = [
|
|
3904
|
-
`--disable-extensions-except=${extensionPath}`,
|
|
3905
|
-
`--load-extension=${extensionPath}`,
|
|
3906
|
-
"--window-position=-32000,-32000",
|
|
3907
|
-
"--window-size=1,1"
|
|
3908
|
-
];
|
|
3909
|
-
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
3910
|
-
if (useWindowsChrome) {
|
|
3911
|
-
try {
|
|
3912
|
-
chromeSession = await launchWindowsChrome(extensionPath, userDataDir, { viewport, ...videoOpts });
|
|
3913
|
-
const contexts = chromeSession.browser.contexts();
|
|
3914
|
-
context = contexts[0] || await chromeSession.browser.newContext({ viewport, ...videoOpts, acceptDownloads: true });
|
|
3915
|
-
} catch {
|
|
3916
|
-
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3917
|
-
headless: false,
|
|
3918
|
-
args: persistentArgs,
|
|
3919
|
-
viewport,
|
|
3920
|
-
...videoOpts,
|
|
3921
|
-
acceptDownloads: true
|
|
3922
|
-
});
|
|
3923
|
-
}
|
|
3924
|
-
} else {
|
|
3925
|
-
context = await import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
3926
|
-
headless: false,
|
|
3927
|
-
args: persistentArgs,
|
|
3928
|
-
viewport,
|
|
3929
|
-
...videoOpts,
|
|
3930
|
-
acceptDownloads: true
|
|
3931
|
-
});
|
|
3932
|
-
}
|
|
3933
|
-
} else {
|
|
3934
|
-
browser = await import_playwright5.chromium.launch({ headless: true });
|
|
3935
|
-
context = await browser.newContext({ viewport, ...videoOpts, ...authOpts, acceptDownloads: true });
|
|
3936
|
-
}
|
|
3937
|
-
return { context, browser, chromeSession };
|
|
3938
|
-
}
|
|
3939
|
-
async function cleanupBrowserSession(session) {
|
|
3940
|
-
if (session.chromeSession) {
|
|
3941
|
-
await session.chromeSession.cleanup();
|
|
3942
|
-
} else {
|
|
3943
|
-
await session.context.close();
|
|
3944
|
-
if (session.browser) await session.browser.close();
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
|
|
3948
4428
|
// src/record.ts
|
|
3949
4429
|
init_lib();
|
|
3950
4430
|
|
|
@@ -3953,16 +4433,16 @@ var import_node_path13 = require("path");
|
|
|
3953
4433
|
init_utils();
|
|
3954
4434
|
|
|
3955
4435
|
// src/browser/step-validator.ts
|
|
3956
|
-
var
|
|
4436
|
+
var import_node_fs9 = require("fs");
|
|
3957
4437
|
var import_node_path12 = require("path");
|
|
3958
4438
|
init_utils();
|
|
3959
4439
|
function findMediaFile(filename, baseDir) {
|
|
3960
4440
|
const name = (0, import_node_path12.basename)(filename);
|
|
3961
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")];
|
|
3962
4442
|
for (const dir of searchDirs) {
|
|
3963
|
-
if (!(0,
|
|
4443
|
+
if (!(0, import_node_fs9.existsSync)(dir)) continue;
|
|
3964
4444
|
try {
|
|
3965
|
-
const files = (0,
|
|
4445
|
+
const files = (0, import_node_fs9.readdirSync)(dir);
|
|
3966
4446
|
if (files.includes(name)) return (0, import_node_path12.join)(dir, name);
|
|
3967
4447
|
const match = files.find((f) => f.toLowerCase() === name.toLowerCase());
|
|
3968
4448
|
if (match) return (0, import_node_path12.join)(dir, match);
|
|
@@ -4038,7 +4518,7 @@ function resolveMediaStep(step, baseDir, label, startTime) {
|
|
|
4038
4518
|
return { success: false, error: `Missing ${label} path`, executionTimeMs: Date.now() - startTime };
|
|
4039
4519
|
}
|
|
4040
4520
|
const specifiedPath = (0, import_node_path12.isAbsolute)(step.value) ? step.value : (0, import_node_path12.resolve)(baseDir, step.value);
|
|
4041
|
-
if ((0,
|
|
4521
|
+
if ((0, import_node_fs9.existsSync)(specifiedPath)) return { success: true, executionTimeMs: Date.now() - startTime };
|
|
4042
4522
|
const foundPath = findMediaFile(step.value, baseDir);
|
|
4043
4523
|
if (foundPath) {
|
|
4044
4524
|
step.value = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4160,7 +4640,7 @@ async function validateAndExecuteStep(page, step, plan, baseDir) {
|
|
|
4160
4640
|
if (!isPlaceholder) {
|
|
4161
4641
|
const normalizedPath = rawPath.startsWith("mnt/") ? `/${rawPath}` : rawPath;
|
|
4162
4642
|
let filePath = (0, import_node_path12.isAbsolute)(normalizedPath) ? normalizedPath : (0, import_node_path12.resolve)(baseDir, normalizedPath);
|
|
4163
|
-
if (!(0,
|
|
4643
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) {
|
|
4164
4644
|
const foundPath = findMediaFile(rawPath, baseDir);
|
|
4165
4645
|
if (foundPath) {
|
|
4166
4646
|
const relativePath = `./${foundPath.replace(`${baseDir}/`, "").replace(`${baseDir}\\`, "")}`;
|
|
@@ -4324,8 +4804,8 @@ async function validateStepsOnPage(plan, config, logError2, baseDir, onStepStart
|
|
|
4324
4804
|
}
|
|
4325
4805
|
|
|
4326
4806
|
// src/browser/meta.ts
|
|
4327
|
-
var
|
|
4328
|
-
var
|
|
4807
|
+
var import_node_fs10 = require("fs");
|
|
4808
|
+
var import_promises10 = require("fs/promises");
|
|
4329
4809
|
var import_node_path14 = require("path");
|
|
4330
4810
|
var RECORDING_FILE = "recording.mp4";
|
|
4331
4811
|
var RECORDING_META_FILE = "recording.meta.json";
|
|
@@ -4334,9 +4814,9 @@ function getRecordingPath(outputDir) {
|
|
|
4334
4814
|
}
|
|
4335
4815
|
async function loadRecordingMeta(outputDir) {
|
|
4336
4816
|
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4337
|
-
if (!(0,
|
|
4817
|
+
if (!(0, import_node_fs10.existsSync)(metaPath)) return null;
|
|
4338
4818
|
try {
|
|
4339
|
-
const content = await (0,
|
|
4819
|
+
const content = await (0, import_promises10.readFile)(metaPath, "utf-8");
|
|
4340
4820
|
return JSON.parse(content);
|
|
4341
4821
|
} catch {
|
|
4342
4822
|
return null;
|
|
@@ -4344,7 +4824,7 @@ async function loadRecordingMeta(outputDir) {
|
|
|
4344
4824
|
}
|
|
4345
4825
|
async function saveRecordingMeta(outputDir, meta) {
|
|
4346
4826
|
const metaPath = (0, import_node_path14.join)(outputDir, RECORDING_META_FILE);
|
|
4347
|
-
await (0,
|
|
4827
|
+
await (0, import_promises10.writeFile)(metaPath, JSON.stringify(meta, null, 2));
|
|
4348
4828
|
}
|
|
4349
4829
|
function canReuseRecording(plan, config, stepDurations, existingMeta) {
|
|
4350
4830
|
if (!existingMeta) {
|
|
@@ -4414,14 +4894,14 @@ function formatStepLabel(step) {
|
|
|
4414
4894
|
}
|
|
4415
4895
|
async function recordSession(plan, config, stepDurations, workDir, baseDir, task2, logError2) {
|
|
4416
4896
|
const videoDir = (0, import_node_path15.join)(workDir, "video");
|
|
4417
|
-
await (0,
|
|
4897
|
+
await (0, import_promises11.mkdir)(videoDir, { recursive: true });
|
|
4418
4898
|
const getStepDuration = (stepNum) => {
|
|
4419
4899
|
return stepDurations[stepNum - 1] ?? config.timing.minStepDuration;
|
|
4420
4900
|
};
|
|
4421
4901
|
const browserTask = task2("Starting browser recording");
|
|
4422
4902
|
const recordingStartTime = Date.now();
|
|
4423
4903
|
const userDataDir = (0, import_node_path15.join)(workDir, "browser-data");
|
|
4424
|
-
const recordVideo = { dir: videoDir, size:
|
|
4904
|
+
const recordVideo = { dir: videoDir, size: recordingViewport(config) };
|
|
4425
4905
|
const authState = findAuthState(plan.url);
|
|
4426
4906
|
const { context, browser, chromeSession } = await launchBrowserContext(config, userDataDir, recordVideo, authState);
|
|
4427
4907
|
const page = context.pages()[0] || await context.newPage();
|
|
@@ -4442,6 +4922,7 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4442
4922
|
let onMediaPage = firstStepIsMedia;
|
|
4443
4923
|
const failures = [];
|
|
4444
4924
|
const actualStepDurations = [];
|
|
4925
|
+
let pageClosedEarly = false;
|
|
4445
4926
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
4446
4927
|
const step = plan.steps[i];
|
|
4447
4928
|
const stepNum = i + 1;
|
|
@@ -4468,6 +4949,12 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4468
4949
|
let targetEl = null;
|
|
4469
4950
|
if (step.target) {
|
|
4470
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
|
+
}
|
|
4471
4958
|
}
|
|
4472
4959
|
if (step.zoom && step.zoom !== 1 && targetEl && !isExtensionPage) {
|
|
4473
4960
|
const preZoomBox = await targetEl.boundingBox() ?? void 0;
|
|
@@ -4504,11 +4991,19 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4504
4991
|
}
|
|
4505
4992
|
const stepElapsed = Date.now() - stepStartTime;
|
|
4506
4993
|
const waitTime = Math.max(0, stepDuration - stepElapsed);
|
|
4507
|
-
if (waitTime > 0) await page.waitForTimeout(waitTime);
|
|
4508
4994
|
const stepDelay = config.video.stepDelay ?? 500;
|
|
4509
4995
|
const isLastStep = i === plan.steps.length - 1;
|
|
4510
|
-
|
|
4511
|
-
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;
|
|
4512
5007
|
}
|
|
4513
5008
|
if (!isExtensionPage) {
|
|
4514
5009
|
const shouldHighlight = step.highlight !== void 0 ? step.highlight : config.video.highlight;
|
|
@@ -4526,13 +5021,14 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4526
5021
|
console.log(import_chalk6.default.yellow(` - ${f}`));
|
|
4527
5022
|
}
|
|
4528
5023
|
}
|
|
5024
|
+
while (actualStepDurations.length < plan.steps.length) actualStepDurations.push(0);
|
|
4529
5025
|
const saveTask = task2("Saving video");
|
|
4530
5026
|
const video = page.video();
|
|
4531
5027
|
if (!video) throw new Error("No video recorder available");
|
|
4532
|
-
await page.close();
|
|
5028
|
+
if (!pageClosedEarly) await page.close();
|
|
4533
5029
|
const videoPath = await video.path();
|
|
4534
5030
|
await cleanupBrowserSession({ context, browser, chromeSession });
|
|
4535
|
-
if (!videoPath || !(0,
|
|
5031
|
+
if (!videoPath || !(0, import_node_fs11.existsSync)(videoPath)) {
|
|
4536
5032
|
throw new Error("Video file was not created");
|
|
4537
5033
|
}
|
|
4538
5034
|
saveTask.succeed("Video saved");
|
|
@@ -4541,8 +5037,8 @@ async function recordSession(plan, config, stepDurations, workDir, baseDir, task
|
|
|
4541
5037
|
|
|
4542
5038
|
// src/utils/video-pipeline.ts
|
|
4543
5039
|
var import_video_kit6 = require("@spark-apps/video-kit");
|
|
4544
|
-
var
|
|
4545
|
-
var
|
|
5040
|
+
var import_node_fs12 = require("fs");
|
|
5041
|
+
var import_promises12 = require("fs/promises");
|
|
4546
5042
|
var import_node_path16 = require("path");
|
|
4547
5043
|
var import_chalk7 = __toESM(require("chalk"));
|
|
4548
5044
|
init_lib();
|
|
@@ -4593,7 +5089,7 @@ async function generateASS(plan, stepAudio, workDir, config, captions) {
|
|
|
4593
5089
|
currentTime += audio.duration;
|
|
4594
5090
|
}
|
|
4595
5091
|
const assPath = (0, import_node_path16.join)(workDir, "subtitles.ass");
|
|
4596
|
-
await (0,
|
|
5092
|
+
await (0, import_promises12.writeFile)(assPath, ass);
|
|
4597
5093
|
return assPath;
|
|
4598
5094
|
}
|
|
4599
5095
|
function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
@@ -4607,7 +5103,7 @@ function buildVideoOverlays(steps, stepDurations, baseDir) {
|
|
|
4607
5103
|
const ext = step.value.split(".").pop()?.toLowerCase() || "";
|
|
4608
5104
|
if (["mp4", "webm", "mov"].includes(ext)) {
|
|
4609
5105
|
const mediaPath = (0, import_node_path16.isAbsolute)(step.value) ? step.value : (0, import_node_path16.resolve)(baseDir, step.value);
|
|
4610
|
-
if ((0,
|
|
5106
|
+
if ((0, import_node_fs12.existsSync)(mediaPath)) {
|
|
4611
5107
|
const partial = parsePartial(step.partial);
|
|
4612
5108
|
overlays.push({
|
|
4613
5109
|
path: mediaPath,
|
|
@@ -4636,13 +5132,13 @@ async function composeAndFinish(videoPath, audioPath, assPath, config, userTier
|
|
|
4636
5132
|
process.exit(0);
|
|
4637
5133
|
}
|
|
4638
5134
|
const composeTask = task("Building final video");
|
|
4639
|
-
await (0,
|
|
5135
|
+
await (0, import_promises12.mkdir)(config.output.dir, { recursive: true });
|
|
4640
5136
|
const suffix = config.lang ? `-${config.lang}` : "";
|
|
4641
5137
|
const finalPath = (0, import_node_path16.join)(config.output.dir, `demo${suffix}.${config.video.format}`);
|
|
4642
5138
|
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
4643
|
-
const hasVoiceover = audioPath && (0,
|
|
5139
|
+
const hasVoiceover = audioPath && (0, import_node_fs12.existsSync)(audioPath);
|
|
4644
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) : "";
|
|
4645
|
-
const hasMusic = musicPath && (0,
|
|
5141
|
+
const hasMusic = musicPath && (0, import_node_fs12.existsSync)(musicPath);
|
|
4646
5142
|
try {
|
|
4647
5143
|
await composeVideo({
|
|
4648
5144
|
videoPath,
|
|
@@ -4707,19 +5203,20 @@ var import_chalk9 = __toESM(require("chalk"));
|
|
|
4707
5203
|
init_lib();
|
|
4708
5204
|
|
|
4709
5205
|
// src/plan.ts
|
|
4710
|
-
var
|
|
5206
|
+
var import_promises13 = require("fs/promises");
|
|
4711
5207
|
var import_node_path17 = require("path");
|
|
4712
5208
|
var import_chalk8 = __toESM(require("chalk"));
|
|
4713
|
-
var
|
|
5209
|
+
var import_playwright5 = require("playwright");
|
|
4714
5210
|
init_lib();
|
|
5211
|
+
init_json_repair();
|
|
4715
5212
|
async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
4716
5213
|
const browserTask = task2("Launching browser with extension");
|
|
4717
5214
|
const userDataDir = (0, import_node_path17.join)(workDir, ".browser-data");
|
|
4718
|
-
await (0,
|
|
5215
|
+
await (0, import_promises13.mkdir)(userDataDir, { recursive: true });
|
|
4719
5216
|
let context;
|
|
4720
5217
|
let chromeSession = null;
|
|
4721
5218
|
const useWindowsChrome = isWSL() && tryEnableWSLInterop();
|
|
4722
|
-
const launchPersistent = () =>
|
|
5219
|
+
const launchPersistent = () => import_playwright5.chromium.launchPersistentContext(userDataDir, {
|
|
4723
5220
|
headless: false,
|
|
4724
5221
|
args: [
|
|
4725
5222
|
`--disable-extensions-except=${extensionPath}`,
|
|
@@ -4800,7 +5297,7 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4800
5297
|
else if (el.type === "select" || el.type === "select-one") action = "select";
|
|
4801
5298
|
else if (el.type === "checkbox") action = "check";
|
|
4802
5299
|
else if (["text", "email", "password", "number", "search", "url", "tel"].includes(el.type)) action = "type";
|
|
4803
|
-
const selector = el.id ?
|
|
5300
|
+
const selector = el.id ? idSelector(el.id) : el.name ? `[name="${el.name}"]` : `text="${el.text}"`;
|
|
4804
5301
|
return {
|
|
4805
5302
|
type: el.type === "button" || el.type === "link" ? el.type : "input",
|
|
4806
5303
|
text: el.text,
|
|
@@ -4812,31 +5309,8 @@ async function crawlExtension(url, extensionPath, workDir, task2) {
|
|
|
4812
5309
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
4813
5310
|
return { title, elements };
|
|
4814
5311
|
}
|
|
4815
|
-
var JSON_FIX_SYSTEM_PROMPT = "You fix malformed JSON. Return ONLY valid JSON, no explanation.";
|
|
4816
|
-
var jsonFixUserPrompt = (content) => `Fix this malformed JSON plan. Return ONLY valid JSON, no explanation.
|
|
4817
|
-
The JSON should have: url, title, description, steps array.
|
|
4818
|
-
Each step has: id (number), action, caption, and optionally target, value.
|
|
4819
|
-
|
|
4820
|
-
Malformed content:
|
|
4821
|
-
${content}`;
|
|
4822
5312
|
async function parsePlanJson(content, task2, logError2, userEmail, tier = "free") {
|
|
4823
|
-
|
|
4824
|
-
return JSON.parse(content);
|
|
4825
|
-
} catch {
|
|
4826
|
-
const fixTask = task2("Fixing malformed JSON");
|
|
4827
|
-
try {
|
|
4828
|
-
const fixed = await callGroq(JSON_FIX_SYSTEM_PROMPT, jsonFixUserPrompt(content), logError2, userEmail, tier);
|
|
4829
|
-
const jsonMatch = fixed.match(/\{[\s\S]*\}/);
|
|
4830
|
-
if (!jsonMatch) throw new Error("No JSON in fix");
|
|
4831
|
-
const plan = JSON.parse(jsonMatch[0]);
|
|
4832
|
-
fixTask.succeed("JSON fixed by AI");
|
|
4833
|
-
return plan;
|
|
4834
|
-
} catch (err) {
|
|
4835
|
-
await logError2("JSON Fix", err);
|
|
4836
|
-
fixTask.fail("Could not fix JSON");
|
|
4837
|
-
throw new Error(`Malformed JSON: ${getErrorMessage(err)}`);
|
|
4838
|
-
}
|
|
4839
|
-
}
|
|
5313
|
+
return parseAIJsonWithRepair(content, { logError: logError2, task: task2, userEmail, tier });
|
|
4840
5314
|
}
|
|
4841
5315
|
async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError2, userEmail, extensionPath, workDir, tier = "free", viewport) {
|
|
4842
5316
|
const { title, elements, scrollable } = extensionPath && workDir ? { ...await crawlExtension(url, extensionPath, workDir, task2), scrollable: void 0 } : await crawlPage(url, task2, findAuthState(url), viewport);
|
|
@@ -4845,10 +5319,9 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4845
5319
|
const systemPrompt = buildSystemPrompt(maxSteps);
|
|
4846
5320
|
const userPrompt = buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable);
|
|
4847
5321
|
try {
|
|
4848
|
-
const content = await callGroq(systemPrompt, userPrompt, logError2, userEmail, tier);
|
|
4849
|
-
const
|
|
4850
|
-
if (!
|
|
4851
|
-
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");
|
|
4852
5325
|
if (lang !== void 0) plan.lang = lang;
|
|
4853
5326
|
plan.steps = plan.steps.map(({ duration, ...s }, i) => ({
|
|
4854
5327
|
...s,
|
|
@@ -4865,7 +5338,7 @@ async function crawlAndAnalyze(url, lang, description, maxSteps, task2, logError
|
|
|
4865
5338
|
}
|
|
4866
5339
|
}
|
|
4867
5340
|
async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier = "free") {
|
|
4868
|
-
const content = await (0,
|
|
5341
|
+
const content = await (0, import_promises13.readFile)(planPath, "utf-8");
|
|
4869
5342
|
const plan = await parsePlanJson(content, task2, logError2, userEmail, tier);
|
|
4870
5343
|
if (!plan.url || !plan.steps || !Array.isArray(plan.steps)) {
|
|
4871
5344
|
throw new Error("Invalid plan: missing url or steps");
|
|
@@ -4898,7 +5371,7 @@ async function validateAndFixPlan(planPath, task2, logError2, userEmail, tier =
|
|
|
4898
5371
|
renumberTask.fail("No valid steps");
|
|
4899
5372
|
throw new Error("Plan has no valid steps");
|
|
4900
5373
|
}
|
|
4901
|
-
await (0,
|
|
5374
|
+
await (0, import_promises13.writeFile)(planPath, JSON.stringify(plan, null, 2));
|
|
4902
5375
|
const removed = originalCount - plan.steps.length;
|
|
4903
5376
|
renumberTask.succeed(
|
|
4904
5377
|
removed > 0 ? `${plan.steps.length} steps (${removed} removed)` : `${plan.steps.length} steps validated`
|
|
@@ -5013,6 +5486,21 @@ function getOutputDirFromUrl(url) {
|
|
|
5013
5486
|
|
|
5014
5487
|
// src/commands/phases/video-phase.ts
|
|
5015
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
|
+
}
|
|
5016
5504
|
async function runVideoPhase(params) {
|
|
5017
5505
|
let { stepAudio } = params;
|
|
5018
5506
|
const { plan, config, voiceoverPath, workDir, captions, userTier } = params;
|
|
@@ -5031,7 +5519,7 @@ async function runVideoPhase(params) {
|
|
|
5031
5519
|
mediaDuration = (trimEnd - trimStart2) * 1e3;
|
|
5032
5520
|
} else {
|
|
5033
5521
|
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5034
|
-
if ((0,
|
|
5522
|
+
if ((0, import_node_fs13.existsSync)(mediaPath)) {
|
|
5035
5523
|
try {
|
|
5036
5524
|
const fullDuration = await getMediaDuration(mediaPath);
|
|
5037
5525
|
const start = (trimStart2 ?? 0) * 1e3;
|
|
@@ -5043,7 +5531,7 @@ async function runVideoPhase(params) {
|
|
|
5043
5531
|
}
|
|
5044
5532
|
} else {
|
|
5045
5533
|
const mediaPath = (0, import_node_path18.resolve)(s.value);
|
|
5046
|
-
if ((0,
|
|
5534
|
+
if ((0, import_node_fs13.existsSync)(mediaPath)) {
|
|
5047
5535
|
try {
|
|
5048
5536
|
mediaDuration = await getMediaDuration(mediaPath);
|
|
5049
5537
|
} catch {
|
|
@@ -5056,7 +5544,7 @@ async function runVideoPhase(params) {
|
|
|
5056
5544
|
})
|
|
5057
5545
|
);
|
|
5058
5546
|
const recordingPath = getRecordingPath(config.output.dir);
|
|
5059
|
-
const hasExistingRecording = existingRecordingMeta && (0,
|
|
5547
|
+
const hasExistingRecording = existingRecordingMeta && (0, import_node_fs13.existsSync)(recordingPath);
|
|
5060
5548
|
let videoPath = recordingPath;
|
|
5061
5549
|
const firstStep = plan.steps[0];
|
|
5062
5550
|
const firstStepIsVideo = firstStep && (firstStep.action === "video" || firstStep.action === "media");
|
|
@@ -5065,7 +5553,7 @@ async function runVideoPhase(params) {
|
|
|
5065
5553
|
header("Record");
|
|
5066
5554
|
const result = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
5067
5555
|
stepAudio = await alignVoiceoverToRecording(stepAudio, result.actualStepDurations, workDir, config.output.dir);
|
|
5068
|
-
await (0,
|
|
5556
|
+
await (0, import_promises14.copyFile)(result.videoPath, recordingPath);
|
|
5069
5557
|
const meta = buildRecordingMeta(plan, config, result.actualStepDurations);
|
|
5070
5558
|
meta.pageLoadTime = result.pageLoadTime;
|
|
5071
5559
|
await saveRecordingMeta(config.output.dir, meta);
|
|
@@ -5117,7 +5605,7 @@ async function runVideoPhase(params) {
|
|
|
5117
5605
|
const transitionTask = task("Applying transitions");
|
|
5118
5606
|
try {
|
|
5119
5607
|
const transitionedPath = (0, import_node_path18.join)(workDir, "transitioned.mp4");
|
|
5120
|
-
const videoDuration =
|
|
5608
|
+
const videoDuration = await getMediaDuration(videoPath) / 1e3;
|
|
5121
5609
|
await applyTransitions({
|
|
5122
5610
|
inputVideo: videoPath,
|
|
5123
5611
|
outputVideo: transitionedPath,
|
|
@@ -5135,30 +5623,24 @@ async function runVideoPhase(params) {
|
|
|
5135
5623
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
5136
5624
|
let videoToCompose = videoPath;
|
|
5137
5625
|
let mainContentDuration;
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
});
|
|
5157
|
-
videoToCompose = combinedPath;
|
|
5158
|
-
} catch (err) {
|
|
5159
|
-
await logError("Concat Outro", err);
|
|
5160
|
-
mainContentDuration = void 0;
|
|
5161
|
-
}
|
|
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;
|
|
5162
5644
|
}
|
|
5163
5645
|
}
|
|
5164
5646
|
let finalPath = await composeAndFinish(
|
|
@@ -5192,7 +5674,7 @@ async function runVideoPhase(params) {
|
|
|
5192
5674
|
}
|
|
5193
5675
|
|
|
5194
5676
|
// src/commands/phases/voiceover-phase.ts
|
|
5195
|
-
var
|
|
5677
|
+
var import_node_fs14 = require("fs");
|
|
5196
5678
|
var import_node_path19 = require("path");
|
|
5197
5679
|
var import_chalk10 = __toESM(require("chalk"));
|
|
5198
5680
|
init_lib();
|
|
@@ -5202,9 +5684,9 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5202
5684
|
const filesToDelete = ["voiceover.json", "voiceover.wav", "recording.meta.json"];
|
|
5203
5685
|
for (const file of filesToDelete) {
|
|
5204
5686
|
const filePath = (0, import_node_path19.join)(config.output.dir, file);
|
|
5205
|
-
if ((0,
|
|
5206
|
-
const { unlink:
|
|
5207
|
-
await
|
|
5687
|
+
if ((0, import_node_fs14.existsSync)(filePath)) {
|
|
5688
|
+
const { unlink: unlink4 } = await import("fs/promises");
|
|
5689
|
+
await unlink4(filePath).catch(() => {
|
|
5208
5690
|
});
|
|
5209
5691
|
}
|
|
5210
5692
|
}
|
|
@@ -5213,7 +5695,12 @@ async function runVoiceoverPhase(plan, config, workDir, skipVoice, fresh) {
|
|
|
5213
5695
|
const voice = resolveVoice(config);
|
|
5214
5696
|
const existingMeta = await loadVoiceoverMeta(config.output.dir);
|
|
5215
5697
|
const voiceoverFile = (0, import_node_path19.join)(config.output.dir, "voiceover.wav");
|
|
5216
|
-
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;
|
|
5217
5704
|
const voiceMatches = !existingMeta || !voice || existingMeta.voice === voice;
|
|
5218
5705
|
let voiceoverPath;
|
|
5219
5706
|
let stepAudio;
|
|
@@ -5303,7 +5790,8 @@ async function captureCommand(url, lang) {
|
|
|
5303
5790
|
lang,
|
|
5304
5791
|
logError,
|
|
5305
5792
|
userData.email,
|
|
5306
|
-
captureResult.pageContext
|
|
5793
|
+
captureResult.pageContext,
|
|
5794
|
+
userData.tier ?? "free"
|
|
5307
5795
|
);
|
|
5308
5796
|
narrateTask.succeed(`${steps.length} captions generated`);
|
|
5309
5797
|
} catch (err) {
|
|
@@ -5348,7 +5836,7 @@ async function captureCommand(url, lang) {
|
|
|
5348
5836
|
}
|
|
5349
5837
|
|
|
5350
5838
|
// src/commands/edit-cmd.ts
|
|
5351
|
-
var
|
|
5839
|
+
var import_node_fs15 = require("fs");
|
|
5352
5840
|
var import_node_path21 = require("path");
|
|
5353
5841
|
var import_chalk12 = __toESM(require("chalk"));
|
|
5354
5842
|
|
|
@@ -5463,10 +5951,10 @@ var PLAN_FILE = /^plan-([a-z]{2}(?:-[A-Z]{2})?)\.json$/;
|
|
|
5463
5951
|
function findPlanFile(dir, lang) {
|
|
5464
5952
|
if (lang) {
|
|
5465
5953
|
const exact = (0, import_node_path21.join)(dir, `plan-${lang}.json`);
|
|
5466
|
-
return (0,
|
|
5954
|
+
return (0, import_node_fs15.existsSync)(exact) ? exact : null;
|
|
5467
5955
|
}
|
|
5468
|
-
if (!(0,
|
|
5469
|
-
const plans = (0,
|
|
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));
|
|
5470
5958
|
const preferred = plans.find((f) => f === "plan-en.json") ?? plans[0];
|
|
5471
5959
|
return preferred ? (0, import_node_path21.join)(dir, preferred) : null;
|
|
5472
5960
|
}
|
|
@@ -5481,7 +5969,7 @@ async function editCommand(url, lang) {
|
|
|
5481
5969
|
console.log();
|
|
5482
5970
|
return;
|
|
5483
5971
|
}
|
|
5484
|
-
const parsed = parsePlanText((0,
|
|
5972
|
+
const parsed = parsePlanText((0, import_node_fs15.readFileSync)(planPath, "utf8"));
|
|
5485
5973
|
if (!parsed.ok) {
|
|
5486
5974
|
console.log(import_chalk12.default.red(` ${planPath} cannot be opened: ${parsed.error}`));
|
|
5487
5975
|
console.log();
|
|
@@ -5504,14 +5992,14 @@ async function editCommand(url, lang) {
|
|
|
5504
5992
|
}
|
|
5505
5993
|
|
|
5506
5994
|
// src/commands/init-cmd.ts
|
|
5507
|
-
var
|
|
5508
|
-
var
|
|
5995
|
+
var import_node_fs16 = require("fs");
|
|
5996
|
+
var import_promises15 = require("fs/promises");
|
|
5509
5997
|
var import_node_path22 = require("path");
|
|
5510
5998
|
var import_chalk13 = __toESM(require("chalk"));
|
|
5511
5999
|
init_lib();
|
|
5512
6000
|
async function initCommand() {
|
|
5513
6001
|
const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
|
|
5514
|
-
if ((0,
|
|
6002
|
+
if ((0, import_node_fs16.existsSync)(configPath)) {
|
|
5515
6003
|
console.log(import_chalk13.default.yellow(` \u26A0 ${CONFIG_FILE} already exists`));
|
|
5516
6004
|
return;
|
|
5517
6005
|
}
|
|
@@ -5535,7 +6023,7 @@ async function initCommand() {
|
|
|
5535
6023
|
volume: 0.3
|
|
5536
6024
|
}
|
|
5537
6025
|
};
|
|
5538
|
-
await (0,
|
|
6026
|
+
await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(defaultConfig, null, 2)}
|
|
5539
6027
|
`);
|
|
5540
6028
|
console.log(import_chalk13.default.green(` \u2714 Created ${CONFIG_FILE}`));
|
|
5541
6029
|
console.log(import_chalk13.default.dim(` Edit it or use: qp config <key> <value>`));
|
|
@@ -5543,11 +6031,11 @@ async function initCommand() {
|
|
|
5543
6031
|
async function configCommand(key, value) {
|
|
5544
6032
|
const configPath = (0, import_node_path22.join)(process.cwd(), CONFIG_FILE);
|
|
5545
6033
|
if (!key) {
|
|
5546
|
-
if (!(0,
|
|
6034
|
+
if (!(0, import_node_fs16.existsSync)(configPath)) {
|
|
5547
6035
|
console.log(import_chalk13.default.yellow(` No ${CONFIG_FILE} found. Run: qp init`));
|
|
5548
6036
|
return;
|
|
5549
6037
|
}
|
|
5550
|
-
const content = await (0,
|
|
6038
|
+
const content = await (0, import_promises15.readFile)(configPath, "utf-8");
|
|
5551
6039
|
console.log(import_chalk13.default.cyan(` ${CONFIG_FILE}:`));
|
|
5552
6040
|
console.log(content);
|
|
5553
6041
|
return;
|
|
@@ -5558,9 +6046,9 @@ async function configCommand(key, value) {
|
|
|
5558
6046
|
return;
|
|
5559
6047
|
}
|
|
5560
6048
|
let config = {};
|
|
5561
|
-
if ((0,
|
|
6049
|
+
if ((0, import_node_fs16.existsSync)(configPath)) {
|
|
5562
6050
|
try {
|
|
5563
|
-
config = JSON.parse(await (0,
|
|
6051
|
+
config = JSON.parse(await (0, import_promises15.readFile)(configPath, "utf-8"));
|
|
5564
6052
|
} catch {
|
|
5565
6053
|
}
|
|
5566
6054
|
}
|
|
@@ -5582,7 +6070,7 @@ async function configCommand(key, value) {
|
|
|
5582
6070
|
}
|
|
5583
6071
|
obj[keys[keys.length - 1]] = parsedValue;
|
|
5584
6072
|
}
|
|
5585
|
-
await (0,
|
|
6073
|
+
await (0, import_promises15.writeFile)(configPath, `${JSON.stringify(config, null, 2)}
|
|
5586
6074
|
`);
|
|
5587
6075
|
console.log(import_chalk13.default.green(` \u2714 Set ${key} = ${value}`));
|
|
5588
6076
|
}
|
|
@@ -5648,8 +6136,8 @@ async function logoutCommand() {
|
|
|
5648
6136
|
}
|
|
5649
6137
|
|
|
5650
6138
|
// src/commands/record-cmd.ts
|
|
5651
|
-
var
|
|
5652
|
-
var
|
|
6139
|
+
var import_node_fs19 = require("fs");
|
|
6140
|
+
var import_promises18 = require("fs/promises");
|
|
5653
6141
|
var import_node_path25 = require("path");
|
|
5654
6142
|
var import_chalk19 = __toESM(require("chalk"));
|
|
5655
6143
|
init_lib();
|
|
@@ -5824,18 +6312,18 @@ async function runAuthPhase(input) {
|
|
|
5824
6312
|
}
|
|
5825
6313
|
|
|
5826
6314
|
// src/commands/phases/plan-phase.ts
|
|
5827
|
-
var
|
|
5828
|
-
var
|
|
6315
|
+
var import_node_fs17 = require("fs");
|
|
6316
|
+
var import_promises16 = require("fs/promises");
|
|
5829
6317
|
var import_node_path23 = require("path");
|
|
5830
6318
|
var import_chalk17 = __toESM(require("chalk"));
|
|
5831
6319
|
|
|
5832
6320
|
// src/ai/fix.ts
|
|
5833
6321
|
init_lib();
|
|
5834
6322
|
async function callGroqAndFinalize(systemPrompt, userMessage, plan, logError2, userEmail, tier = "free") {
|
|
5835
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
5836
|
-
const
|
|
5837
|
-
if (!
|
|
5838
|
-
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);
|
|
5839
6327
|
}
|
|
5840
6328
|
function finalizePlan(result, original) {
|
|
5841
6329
|
result.url = original.url;
|
|
@@ -5959,10 +6447,15 @@ async function translatePlan(plan, targetLang, logError2, userEmail, tier = "fre
|
|
|
5959
6447
|
${JSON.stringify(toTranslate)}
|
|
5960
6448
|
|
|
5961
6449
|
Return ONLY JSON: {"description": "...", "captions": ["...", ...]}`;
|
|
5962
|
-
const content = await callGroq(systemPrompt, userMessage, logError2, userEmail, tier);
|
|
5963
|
-
const
|
|
5964
|
-
|
|
5965
|
-
|
|
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");
|
|
5966
6459
|
const translatedPlan = JSON.parse(JSON.stringify(plan));
|
|
5967
6460
|
translatedPlan.title = plan.title;
|
|
5968
6461
|
translatedPlan.description = translated.description || plan.description;
|
|
@@ -5994,9 +6487,9 @@ async function runPlanPhase(params) {
|
|
|
5994
6487
|
const extensionPath = config.browser?.extension;
|
|
5995
6488
|
let existingPlan = null;
|
|
5996
6489
|
let existingPlanPath = null;
|
|
5997
|
-
if ((0,
|
|
6490
|
+
if ((0, import_node_fs17.existsSync)(planPath)) {
|
|
5998
6491
|
try {
|
|
5999
|
-
const parsed = JSON.parse(await (0,
|
|
6492
|
+
const parsed = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
6000
6493
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
6001
6494
|
existingPlan = parsed;
|
|
6002
6495
|
existingPlanPath = planPath;
|
|
@@ -6006,12 +6499,12 @@ async function runPlanPhase(params) {
|
|
|
6006
6499
|
}
|
|
6007
6500
|
if (!existingPlan) {
|
|
6008
6501
|
try {
|
|
6009
|
-
const files = (0,
|
|
6502
|
+
const files = (0, import_node_fs17.readdirSync)(config.output.dir);
|
|
6010
6503
|
const planFiles = files.filter((f) => /^plan-[a-z]{2}(-[A-Z]{2})?\.json$/.test(f) && f !== (0, import_node_path23.basename)(planPath));
|
|
6011
6504
|
for (const file of planFiles) {
|
|
6012
6505
|
const filePath = (0, import_node_path23.join)(config.output.dir, file);
|
|
6013
6506
|
try {
|
|
6014
|
-
const parsed = JSON.parse(await (0,
|
|
6507
|
+
const parsed = JSON.parse(await (0, import_promises16.readFile)(filePath, "utf-8"));
|
|
6015
6508
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
6016
6509
|
existingPlan = parsed;
|
|
6017
6510
|
existingPlanPath = filePath;
|
|
@@ -6023,9 +6516,9 @@ async function runPlanPhase(params) {
|
|
|
6023
6516
|
} catch {
|
|
6024
6517
|
}
|
|
6025
6518
|
}
|
|
6026
|
-
if (!existingPlan && (0,
|
|
6519
|
+
if (!existingPlan && (0, import_node_fs17.existsSync)(legacyPlanPath)) {
|
|
6027
6520
|
try {
|
|
6028
|
-
const parsed = JSON.parse(await (0,
|
|
6521
|
+
const parsed = JSON.parse(await (0, import_promises16.readFile)(legacyPlanPath, "utf-8"));
|
|
6029
6522
|
if (parsed.url && parsed.steps?.length > 0) {
|
|
6030
6523
|
existingPlan = parsed;
|
|
6031
6524
|
existingPlanPath = legacyPlanPath;
|
|
@@ -6035,6 +6528,11 @@ async function runPlanPhase(params) {
|
|
|
6035
6528
|
}
|
|
6036
6529
|
const normalizeUrl4 = (u) => u.replace(/\/+$/, "").toLowerCase();
|
|
6037
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
|
+
}
|
|
6038
6536
|
let plan;
|
|
6039
6537
|
const doTranslate = async (failMsg) => {
|
|
6040
6538
|
const translateTask = task(`Translating to ${targetLang}`);
|
|
@@ -6168,9 +6666,9 @@ async function runPlanPhase(params) {
|
|
|
6168
6666
|
const planLang = plan.lang || "en";
|
|
6169
6667
|
if (planLang !== targetLang) {
|
|
6170
6668
|
let useExistingTranslation = false;
|
|
6171
|
-
if ((0,
|
|
6669
|
+
if ((0, import_node_fs17.existsSync)(planPath) && existingPlanPath !== planPath) {
|
|
6172
6670
|
try {
|
|
6173
|
-
const translatedPlan = JSON.parse(await (0,
|
|
6671
|
+
const translatedPlan = JSON.parse(await (0, import_promises16.readFile)(planPath, "utf-8"));
|
|
6174
6672
|
if (translatedPlan.steps?.length === plan.steps.length && translatedPlan.lang === targetLang) {
|
|
6175
6673
|
console.log();
|
|
6176
6674
|
console.log(import_chalk17.default.dim(` Found translated plan: ${formatClickablePath(planPath)}`));
|
|
@@ -6194,7 +6692,7 @@ async function runPlanPhase(params) {
|
|
|
6194
6692
|
}
|
|
6195
6693
|
|
|
6196
6694
|
// src/commands/phases/validation-phase.ts
|
|
6197
|
-
var
|
|
6695
|
+
var import_promises17 = require("fs/promises");
|
|
6198
6696
|
var import_chalk18 = __toESM(require("chalk"));
|
|
6199
6697
|
async function runValidationPhase(params) {
|
|
6200
6698
|
let { plan } = params;
|
|
@@ -6321,7 +6819,7 @@ async function runValidationPhase(params) {
|
|
|
6321
6819
|
console.log(import_chalk18.default.yellow(` Skipping step ${stepNum} - may fail during recording`));
|
|
6322
6820
|
continue;
|
|
6323
6821
|
}
|
|
6324
|
-
plan = JSON.parse(await (0,
|
|
6822
|
+
plan = JSON.parse(await (0, import_promises17.readFile)(planPath, "utf-8"));
|
|
6325
6823
|
const finalRevalidateTask = task(`Final validation of step ${stepNum}`);
|
|
6326
6824
|
const finalRevalidation = await validateStepsOnPage(plan, config, logError, config.output.dir);
|
|
6327
6825
|
const finalResult = finalRevalidation.results[result.stepIndex];
|
|
@@ -6345,7 +6843,7 @@ async function runValidationPhase(params) {
|
|
|
6345
6843
|
}
|
|
6346
6844
|
|
|
6347
6845
|
// src/commands/utils/chrome-extension.ts
|
|
6348
|
-
var
|
|
6846
|
+
var import_node_fs18 = require("fs");
|
|
6349
6847
|
var import_node_os3 = require("os");
|
|
6350
6848
|
var import_node_path24 = require("path");
|
|
6351
6849
|
function resolveChromeExtension(extensionId) {
|
|
@@ -6354,7 +6852,7 @@ function resolveChromeExtension(extensionId) {
|
|
|
6354
6852
|
const getChromeExtDirs = (userDataPath) => {
|
|
6355
6853
|
const dirs = [];
|
|
6356
6854
|
try {
|
|
6357
|
-
const profiles = (0,
|
|
6855
|
+
const profiles = (0, import_node_fs18.readdirSync)(userDataPath).filter((p) => p === "Default" || p.startsWith("Profile "));
|
|
6358
6856
|
for (const profile of profiles) {
|
|
6359
6857
|
dirs.push((0, import_node_path24.join)(userDataPath, profile, "Extensions"));
|
|
6360
6858
|
}
|
|
@@ -6369,7 +6867,7 @@ function resolveChromeExtension(extensionId) {
|
|
|
6369
6867
|
possibleDirs.push(...getChromeExtDirs((0, import_node_path24.join)((0, import_node_os3.homedir)(), "Library", "Application Support", "Google", "Chrome")));
|
|
6370
6868
|
} else {
|
|
6371
6869
|
try {
|
|
6372
|
-
const windowsUsers = (0,
|
|
6870
|
+
const windowsUsers = (0, import_node_fs18.readdirSync)("/mnt/c/Users").filter(
|
|
6373
6871
|
(u) => !["All Users", "Default", "Default User", "Public", "desktop.ini"].includes(u)
|
|
6374
6872
|
);
|
|
6375
6873
|
for (const user of windowsUsers) {
|
|
@@ -6381,18 +6879,18 @@ function resolveChromeExtension(extensionId) {
|
|
|
6381
6879
|
}
|
|
6382
6880
|
for (const chromeExtDir of possibleDirs) {
|
|
6383
6881
|
const extDir = (0, import_node_path24.join)(chromeExtDir, extensionId);
|
|
6384
|
-
if (!(0,
|
|
6882
|
+
if (!(0, import_node_fs18.existsSync)(extDir)) continue;
|
|
6385
6883
|
try {
|
|
6386
|
-
const versions = (0,
|
|
6884
|
+
const versions = (0, import_node_fs18.readdirSync)(extDir).filter((f) => !f.startsWith("."));
|
|
6387
6885
|
if (versions.length === 0) continue;
|
|
6388
6886
|
const latestVersion = versions.sort().pop();
|
|
6389
6887
|
const extPath = (0, import_node_path24.join)(extDir, latestVersion);
|
|
6390
6888
|
let name = extensionId;
|
|
6391
6889
|
let popupPath = "popup.html";
|
|
6392
6890
|
const manifestPath = (0, import_node_path24.join)(extPath, "manifest.json");
|
|
6393
|
-
if ((0,
|
|
6891
|
+
if ((0, import_node_fs18.existsSync)(manifestPath)) {
|
|
6394
6892
|
try {
|
|
6395
|
-
const manifest = JSON.parse((0,
|
|
6893
|
+
const manifest = JSON.parse((0, import_node_fs18.readFileSync)(manifestPath, "utf-8"));
|
|
6396
6894
|
if (manifest.name && !manifest.name.startsWith("__MSG_")) {
|
|
6397
6895
|
name = manifest.name;
|
|
6398
6896
|
}
|
|
@@ -6434,6 +6932,13 @@ async function recordCommand(url, options = {}) {
|
|
|
6434
6932
|
console.log(import_chalk19.default.dim(` Extension: ${chromeExtension.name}`));
|
|
6435
6933
|
console.log(import_chalk19.default.dim(` Path: ${chromeExtension.path}`));
|
|
6436
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
|
+
}
|
|
6941
|
+
}
|
|
6437
6942
|
config.output.dir = chromeExtension ? `./${chromeExtension.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-demo` : getOutputDirFromUrl(url);
|
|
6438
6943
|
const userData = await loadUserData();
|
|
6439
6944
|
const authResult = await runAuthPhase({
|
|
@@ -6467,7 +6972,7 @@ async function recordCommand(url, options = {}) {
|
|
|
6467
6972
|
if (langOverride) config.lang = langOverride;
|
|
6468
6973
|
if (config.music?.path) {
|
|
6469
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);
|
|
6470
|
-
if (!(0,
|
|
6975
|
+
if (!(0, import_node_fs19.existsSync)(musicPath)) {
|
|
6471
6976
|
console.log();
|
|
6472
6977
|
console.log(import_chalk19.default.yellow(` \u26A0 Music file not found: ${config.music.path}`));
|
|
6473
6978
|
console.log(import_chalk19.default.dim(` Config: ${(0, import_node_path25.join)(process.cwd(), CONFIG_FILE)}`));
|
|
@@ -6489,7 +6994,7 @@ async function recordCommand(url, options = {}) {
|
|
|
6489
6994
|
const planPath = (0, import_node_path25.join)(config.output.dir, `plan-${targetLang}.json`);
|
|
6490
6995
|
const legacyPlanPath = (0, import_node_path25.join)(config.output.dir, "plan.json");
|
|
6491
6996
|
const isExtensionUrl = url.startsWith("chrome-extension://");
|
|
6492
|
-
await (0,
|
|
6997
|
+
await (0, import_promises18.mkdir)(config.output.dir, { recursive: true });
|
|
6493
6998
|
await setLogFilePath(config.output.dir);
|
|
6494
6999
|
try {
|
|
6495
7000
|
const plan = await runPlanPhase({
|
|
@@ -6546,31 +7051,67 @@ async function recordCommand(url, options = {}) {
|
|
|
6546
7051
|
}
|
|
6547
7052
|
|
|
6548
7053
|
// src/commands/script-cmd.ts
|
|
6549
|
-
var
|
|
7054
|
+
var import_promises19 = require("fs/promises");
|
|
6550
7055
|
var import_node_path26 = require("path");
|
|
6551
7056
|
var import_chalk20 = __toESM(require("chalk"));
|
|
6552
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
|
+
}
|
|
6553
7069
|
async function scriptCommand(url, scriptPath, lang) {
|
|
7070
|
+
url = resolveStartUrl(url);
|
|
6554
7071
|
const { config, workDir, captions } = await initCommandEnv(url, lang);
|
|
7072
|
+
const userData = await loadUserData();
|
|
7073
|
+
const userTier = userData.email ? await getTierByEmail(userData.email) : "free";
|
|
6555
7074
|
header("Script");
|
|
6556
|
-
const raw = await (0,
|
|
7075
|
+
const raw = await (0, import_promises19.readFile)(scriptPath, "utf-8");
|
|
6557
7076
|
const lines = raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
6558
7077
|
if (lines.length === 0) {
|
|
6559
7078
|
console.log(import_chalk20.default.red(" Script file is empty or has no non-comment lines"));
|
|
6560
7079
|
process.exit(1);
|
|
6561
7080
|
}
|
|
6562
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
|
+
}
|
|
6563
7089
|
header("Analyze");
|
|
6564
7090
|
const crawlTask = task("Crawling page for UI elements");
|
|
6565
|
-
const { elements } = await crawlPage(url, () => crawlTask);
|
|
7091
|
+
const { elements } = await crawlPage(url, () => crawlTask, findAuthState(url));
|
|
6566
7092
|
const elementsText = elements.slice(0, 30).map((e) => ` ${e.action}: ${e.text} (${e.selector})`).join("\n");
|
|
6567
7093
|
header("Plan");
|
|
6568
7094
|
const planTask = task("Mapping script to UI actions");
|
|
6569
7095
|
const appTitle = new URL(url).hostname.replace(/^www\./, "");
|
|
6570
|
-
const plan = await generatePlanFromScript(lines, url, appTitle, elementsText, lang, logError);
|
|
6571
7096
|
const planPath = (0, import_node_path26.join)(config.output.dir, `plan-${lang ?? "en"}.json`);
|
|
6572
|
-
await
|
|
6573
|
-
|
|
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
|
+
}
|
|
6574
7115
|
header("Voiceover");
|
|
6575
7116
|
const { voiceoverPath, stepAudio } = await generateVoiceover(
|
|
6576
7117
|
plan.steps,
|
|
@@ -6586,7 +7127,7 @@ async function scriptCommand(url, scriptPath, lang) {
|
|
|
6586
7127
|
const { videoPath } = await recordSession(plan, config, stepDurations, workDir, config.output.dir, task, logError);
|
|
6587
7128
|
header("Compose");
|
|
6588
7129
|
const assPath = await generateASS(plan, stepAudio, workDir, config, captions);
|
|
6589
|
-
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config,
|
|
7130
|
+
const finalPath = await composeAndFinish(videoPath, voiceoverPath, assPath, config, userTier);
|
|
6590
7131
|
console.log();
|
|
6591
7132
|
console.log(import_chalk20.default.bold.green(" Done!"));
|
|
6592
7133
|
console.log(import_chalk20.default.cyan(` ${finalPath}`));
|
|
@@ -6696,7 +7237,9 @@ function parseArgs(args) {
|
|
|
6696
7237
|
let fresh = false;
|
|
6697
7238
|
for (let i = 0; i < args.length; i++) {
|
|
6698
7239
|
const arg2 = args[i];
|
|
6699
|
-
if (arg2.startsWith("
|
|
7240
|
+
if (!url && (arg2.startsWith("http://") || arg2.startsWith("https://"))) {
|
|
7241
|
+
url = normalizeUrl3(arg2);
|
|
7242
|
+
} else if (arg2.startsWith("lang=")) {
|
|
6700
7243
|
lang = arg2.slice(5);
|
|
6701
7244
|
} else if (arg2 === "--script" || arg2 === "-s") {
|
|
6702
7245
|
scriptPath = args[i + 1];
|