@spark-apps/quickpeek 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -204
- package/dist/highlight.css +15 -15
- package/dist/index.d.mts +79 -3
- package/dist/index.mjs +284 -65
- package/dist/mcp-tools.mjs +1861 -872
- package/dist/qp.js +1606 -838
- package/dist/{scraping-BK8pIJhm.d.mts → scraping-BgepklP3.d.mts} +51 -6
- package/dist/web.d.mts +42 -2
- package/dist/web.mjs +174 -37
- package/mcp.mjs +28 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -39,7 +39,7 @@ function getErrorMessage(err) {
|
|
|
39
39
|
return err instanceof Error ? err.message : String(err);
|
|
40
40
|
}
|
|
41
41
|
function toTitleCase(text) {
|
|
42
|
-
return text.replace(/\w
|
|
42
|
+
return text.replace(/\w+(?:'\w+)*/g, (word, index) => {
|
|
43
43
|
if (index === 0) {
|
|
44
44
|
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
45
45
|
}
|
|
@@ -347,8 +347,39 @@ function getMediaDuration(filePath) {
|
|
|
347
347
|
proc.on("error", reject);
|
|
348
348
|
});
|
|
349
349
|
}
|
|
350
|
+
function getAudioCodec(filePath) {
|
|
351
|
+
return new Promise((resolve3) => {
|
|
352
|
+
const args = [
|
|
353
|
+
"-v",
|
|
354
|
+
"error",
|
|
355
|
+
"-select_streams",
|
|
356
|
+
"a:0",
|
|
357
|
+
"-show_entries",
|
|
358
|
+
"stream=codec_name",
|
|
359
|
+
"-of",
|
|
360
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
361
|
+
filePath
|
|
362
|
+
];
|
|
363
|
+
const proc = spawnFFmpeg("ffprobe", args);
|
|
364
|
+
let stdout = "";
|
|
365
|
+
proc.stdout.on("data", (data) => {
|
|
366
|
+
stdout += data;
|
|
367
|
+
});
|
|
368
|
+
proc.on("close", (code) => resolve3(code === 0 ? stdout.trim() : ""));
|
|
369
|
+
proc.on("error", () => resolve3(""));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
async function isHonestWav(filePath) {
|
|
373
|
+
const codec = await getAudioCodec(filePath);
|
|
374
|
+
return codec.startsWith("pcm_");
|
|
375
|
+
}
|
|
376
|
+
function buildConcatArgs(listPath, output) {
|
|
377
|
+
const isWav = output.toLowerCase().endsWith(".wav");
|
|
378
|
+
const codec = isWav ? ["-c:a", "pcm_s16le"] : ["-c", "copy"];
|
|
379
|
+
return ["-y", "-f", "concat", "-safe", "0", "-i", listPath, ...codec, output];
|
|
380
|
+
}
|
|
350
381
|
function concatMedia(listPath, output) {
|
|
351
|
-
return runFFmpeg(
|
|
382
|
+
return runFFmpeg(buildConcatArgs(listPath, output), "ffmpeg concat failed");
|
|
352
383
|
}
|
|
353
384
|
function generateSilence(outputPath, durationMs) {
|
|
354
385
|
return runFFmpeg(
|
|
@@ -775,13 +806,31 @@ var init_timing = __esm({
|
|
|
775
806
|
|
|
776
807
|
// src/lib/api-client.ts
|
|
777
808
|
init_utils();
|
|
809
|
+
var SERVER_BACKOFF_MS = [2e3, 4e3];
|
|
810
|
+
var CLI_BACKOFF_MS = [1e4, 3e4];
|
|
778
811
|
var SPARK_AI_BASE = "https://sparkbrain.app";
|
|
779
812
|
var SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
780
813
|
var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
781
814
|
var RELAY_BASE = process.env.QUICKPEEK_API_BASE || "https://quickpeek.co";
|
|
782
815
|
var RELAY_TOKEN_URL = `${RELAY_BASE}/api/cli/token`;
|
|
783
816
|
var RELAY_CHAT_URL = `${RELAY_BASE}/api/cli/ai`;
|
|
784
|
-
|
|
817
|
+
function apiKey() {
|
|
818
|
+
return process.env.SPARK_AI_KEY || "";
|
|
819
|
+
}
|
|
820
|
+
function hostOf(url) {
|
|
821
|
+
try {
|
|
822
|
+
return new URL(url).host;
|
|
823
|
+
} catch {
|
|
824
|
+
return "AI service";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
function firstLine(text) {
|
|
828
|
+
return truncate((text ?? "").trim().split("\n")[0] ?? "", 200);
|
|
829
|
+
}
|
|
830
|
+
function truncate(text, max) {
|
|
831
|
+
const trimmed = (text ?? "").trim();
|
|
832
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}\u2026` : trimmed;
|
|
833
|
+
}
|
|
785
834
|
var cachedDirectToken = null;
|
|
786
835
|
var cachedRelayToken = null;
|
|
787
836
|
async function getOrRefreshDirectToken() {
|
|
@@ -792,7 +841,7 @@ async function getOrRefreshDirectToken() {
|
|
|
792
841
|
try {
|
|
793
842
|
const response = await fetch(SPARK_AI_TOKEN, {
|
|
794
843
|
method: "POST",
|
|
795
|
-
headers: { "X-Api-Key":
|
|
844
|
+
headers: { "X-Api-Key": apiKey() }
|
|
796
845
|
});
|
|
797
846
|
if (!response.ok) {
|
|
798
847
|
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
|
|
@@ -835,8 +884,10 @@ async function getOrRefreshRelayToken(email) {
|
|
|
835
884
|
throw new Error(`Failed to get QuickPeek token: ${getErrorMessage(err)}`);
|
|
836
885
|
}
|
|
837
886
|
}
|
|
838
|
-
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier) {
|
|
839
|
-
|
|
887
|
+
async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier, opts = {}) {
|
|
888
|
+
const { onRetry, backoffMs = SERVER_BACKOFF_MS } = opts;
|
|
889
|
+
let lastRetriableError = null;
|
|
890
|
+
const host = hostOf(chatUrl);
|
|
840
891
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
841
892
|
try {
|
|
842
893
|
const response = await fetch(chatUrl, {
|
|
@@ -852,36 +903,53 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
852
903
|
]
|
|
853
904
|
})
|
|
854
905
|
});
|
|
906
|
+
const bodyText = await response.text().catch(() => "");
|
|
855
907
|
let data = {};
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
908
|
+
try {
|
|
909
|
+
data = bodyText ? JSON.parse(bodyText) : {};
|
|
910
|
+
} catch {
|
|
859
911
|
}
|
|
860
912
|
if (!response.ok) {
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
913
|
+
const upstream = data.error || firstLine(bodyText) || response.statusText || "no error body";
|
|
914
|
+
const error = `${host} ${response.status}: ${upstream}`;
|
|
915
|
+
const detail = truncate(bodyText, 800);
|
|
916
|
+
const isRetriable = response.status === 429 || response.status >= 500;
|
|
917
|
+
if (isRetriable && attempt < 3) {
|
|
918
|
+
const retryAfter2 = response.headers.get("Retry-After");
|
|
919
|
+
const delayMs = retryAfter2 ? parseInt(retryAfter2, 10) * 1e3 : response.status >= 500 ? backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1] ?? 2e3 : 1e3 * 2 ** (attempt - 1);
|
|
920
|
+
onRetry?.({ attempt, status: response.status, delayMs, host });
|
|
921
|
+
lastRetriableError = {
|
|
866
922
|
success: false,
|
|
867
923
|
error,
|
|
868
|
-
isRateLimit:
|
|
869
|
-
|
|
924
|
+
isRateLimit: response.status === 429,
|
|
925
|
+
status: response.status,
|
|
926
|
+
host,
|
|
927
|
+
...detail ? { detail } : {},
|
|
928
|
+
...retryAfter2 ? { retryAfterSeconds: parseInt(retryAfter2, 10) } : {}
|
|
870
929
|
};
|
|
871
930
|
await new Promise((resolve3) => setTimeout(resolve3, Math.min(delayMs, 3e4)));
|
|
872
931
|
continue;
|
|
873
932
|
}
|
|
933
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
934
|
+
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
874
935
|
return {
|
|
875
936
|
success: false,
|
|
876
937
|
error,
|
|
877
938
|
isRateLimit: response.status === 429,
|
|
878
|
-
isAuthError: response.status === 401 || response.status === 403
|
|
939
|
+
isAuthError: response.status === 401 || response.status === 403,
|
|
940
|
+
status: response.status,
|
|
941
|
+
host,
|
|
942
|
+
...detail ? { detail } : {},
|
|
943
|
+
...Number.isFinite(retryAfterSeconds) ? { retryAfterSeconds } : {}
|
|
879
944
|
};
|
|
880
945
|
}
|
|
881
946
|
if (!data.success || !data.response) {
|
|
882
947
|
return {
|
|
883
948
|
success: false,
|
|
884
|
-
error: data.error || "
|
|
949
|
+
error: `${host} 200 but no content: ${data.error || firstLine(bodyText) || "empty response"}`,
|
|
950
|
+
status: response.status,
|
|
951
|
+
host,
|
|
952
|
+
...bodyText ? { detail: truncate(bodyText, 800) } : {}
|
|
885
953
|
};
|
|
886
954
|
}
|
|
887
955
|
return {
|
|
@@ -895,15 +963,17 @@ async function executeChat(chatUrl, bearerToken, systemPrompt, userPrompt, tier)
|
|
|
895
963
|
}
|
|
896
964
|
};
|
|
897
965
|
} catch (err) {
|
|
898
|
-
if (attempt === 3)
|
|
966
|
+
if (attempt === 3) {
|
|
967
|
+
return { success: false, error: `${host} unreachable: ${getErrorMessage(err)}`, host };
|
|
968
|
+
}
|
|
899
969
|
const delayMs = 1e3 * 2 ** (attempt - 1);
|
|
900
970
|
await new Promise((resolve3) => setTimeout(resolve3, delayMs));
|
|
901
971
|
}
|
|
902
972
|
}
|
|
903
|
-
return
|
|
973
|
+
return lastRetriableError || { success: false, error: `${host}: request failed after retries`, host };
|
|
904
974
|
}
|
|
905
|
-
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
906
|
-
if (!
|
|
975
|
+
async function callAI(systemPrompt, userPrompt, tier = "free", opts = {}) {
|
|
976
|
+
if (!apiKey()) {
|
|
907
977
|
return {
|
|
908
978
|
success: false,
|
|
909
979
|
error: "AI service is not configured. Please report this issue.",
|
|
@@ -912,12 +982,12 @@ async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
|
912
982
|
}
|
|
913
983
|
try {
|
|
914
984
|
const token = await getOrRefreshDirectToken();
|
|
915
|
-
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier);
|
|
985
|
+
return await executeChat(SPARK_AI_CHAT, token, systemPrompt, userPrompt, tier, opts);
|
|
916
986
|
} catch (err) {
|
|
917
987
|
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
918
988
|
}
|
|
919
989
|
}
|
|
920
|
-
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
990
|
+
async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free", opts = {}) {
|
|
921
991
|
if (!email) {
|
|
922
992
|
return {
|
|
923
993
|
success: false,
|
|
@@ -927,11 +997,11 @@ async function callAIViaRelay(systemPrompt, userPrompt, email, tier = "free") {
|
|
|
927
997
|
}
|
|
928
998
|
try {
|
|
929
999
|
let token = await getOrRefreshRelayToken(email);
|
|
930
|
-
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
1000
|
+
let result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
931
1001
|
if (!result.success && result.isAuthError) {
|
|
932
1002
|
cachedRelayToken = null;
|
|
933
1003
|
token = await getOrRefreshRelayToken(email);
|
|
934
|
-
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier);
|
|
1004
|
+
result = await executeChat(RELAY_CHAT_URL, token, systemPrompt, userPrompt, tier, opts);
|
|
935
1005
|
}
|
|
936
1006
|
return result;
|
|
937
1007
|
} catch (err) {
|
|
@@ -966,7 +1036,8 @@ function normalizeUrl(input) {
|
|
|
966
1036
|
|
|
967
1037
|
// src/lib/SparkStripe-feature-gating.ts
|
|
968
1038
|
var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
969
|
-
var
|
|
1039
|
+
var OWNER_SLUG = process.env.SPARK_OWNER_SLUG ?? "spark";
|
|
1040
|
+
var BASE = "https://sparkpay.dev";
|
|
970
1041
|
var TIERS = {
|
|
971
1042
|
price_1TADnPEf9opkuWid9NtqOYON: "pro",
|
|
972
1043
|
price_1TADnPEf9opkuWidgbvvX9ma: "pro",
|
|
@@ -1009,7 +1080,7 @@ async function getStatusCached(email) {
|
|
|
1009
1080
|
function getTier(d) {
|
|
1010
1081
|
if (!d.subscription || !d.registered) return "free";
|
|
1011
1082
|
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
1012
|
-
return TIERS[d.subscription.price_id] ?? "
|
|
1083
|
+
return TIERS[d.subscription.price_id] ?? "pro";
|
|
1013
1084
|
}
|
|
1014
1085
|
var isPaid = (d) => getTier(d) !== "free";
|
|
1015
1086
|
var hasAccess = (d, min) => (RANK[getTier(d)] ?? 0) >= (RANK[min] ?? 0);
|
|
@@ -1031,7 +1102,7 @@ async function isVerified(email) {
|
|
|
1031
1102
|
}
|
|
1032
1103
|
}
|
|
1033
1104
|
function pricingUrl(opts) {
|
|
1034
|
-
const u = new URL(`${BASE}
|
|
1105
|
+
const u = new URL(`${BASE}/${OWNER_SLUG}/${APP_ID}`);
|
|
1035
1106
|
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
1036
1107
|
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
1037
1108
|
return u.toString();
|
|
@@ -1046,7 +1117,7 @@ function getUpgradeUrl(userEmail) {
|
|
|
1046
1117
|
}
|
|
1047
1118
|
|
|
1048
1119
|
// src/lib/config.ts
|
|
1049
|
-
var VERSION = "1.
|
|
1120
|
+
var VERSION = "1.2.1";
|
|
1050
1121
|
var CONFIG_FILE = "quickpeek.config.json";
|
|
1051
1122
|
var DEFAULT_CONFIG = {
|
|
1052
1123
|
plan: { maxSteps: 8 },
|
|
@@ -1191,6 +1262,9 @@ function shouldSkipLink(text, href, currentPath) {
|
|
|
1191
1262
|
}
|
|
1192
1263
|
|
|
1193
1264
|
// src/lib/crawl.ts
|
|
1265
|
+
function idSelector(id) {
|
|
1266
|
+
return /^[A-Za-z_-][\w-]*$/.test(id) ? `#${id}` : `[id="${id.replace(/(["\\])/g, "\\$1")}"]`;
|
|
1267
|
+
}
|
|
1194
1268
|
function getActionForType(type) {
|
|
1195
1269
|
switch (type) {
|
|
1196
1270
|
case "button":
|
|
@@ -1213,20 +1287,29 @@ var noopTask = () => ({ succeed: () => {
|
|
|
1213
1287
|
var CRAWL_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1214
1288
|
var CRAWL_CACHE_MAX_ENTRIES = 50;
|
|
1215
1289
|
var crawlCache = /* @__PURE__ */ new Map();
|
|
1216
|
-
async function crawlPage(url, task = noopTask, storageStatePath) {
|
|
1217
|
-
const
|
|
1290
|
+
async function crawlPage(url, task = noopTask, storageStatePath, viewport) {
|
|
1291
|
+
const cacheKey = viewport ? `${url}@${viewport.width}x${viewport.height}` : url;
|
|
1292
|
+
const cached = crawlCache.get(cacheKey);
|
|
1218
1293
|
if (cached && Date.now() - cached.timestamp < CRAWL_CACHE_TTL_MS) {
|
|
1219
1294
|
task("Using cached crawl").succeed(`Found ${cached.elements.length} interactive elements (cached)`);
|
|
1220
|
-
return { title: cached.title, elements: [...cached.elements] };
|
|
1295
|
+
return { title: cached.title, elements: [...cached.elements], scrollable: cached.scrollable };
|
|
1221
1296
|
}
|
|
1222
1297
|
const browserTask = task("Launching browser");
|
|
1223
1298
|
const browser = await chromium.launch({ headless: true });
|
|
1224
|
-
const context = await browser.newContext(
|
|
1299
|
+
const context = await browser.newContext({
|
|
1300
|
+
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
1301
|
+
// Crawl at the size we will record at, or the measurements below describe
|
|
1302
|
+
// a page nobody will ever see.
|
|
1303
|
+
...viewport ? { viewport } : {}
|
|
1304
|
+
});
|
|
1225
1305
|
const page = await context.newPage();
|
|
1226
1306
|
browserTask.succeed(storageStatePath ? "Browser launched (signed in)" : "Browser launched");
|
|
1227
1307
|
const crawlTask = task("Crawling page");
|
|
1228
1308
|
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
1229
1309
|
const title = await page.title();
|
|
1310
|
+
const scrollable = await page.evaluate(
|
|
1311
|
+
"Math.max(0, document.documentElement.scrollHeight - window.innerHeight)"
|
|
1312
|
+
);
|
|
1230
1313
|
const rawElements = await page.evaluate(`
|
|
1231
1314
|
(() => {
|
|
1232
1315
|
const items = [];
|
|
@@ -1316,9 +1399,9 @@ async function crawlPage(url, task = noopTask, storageStatePath) {
|
|
|
1316
1399
|
} else if (el.isFile || el.type === "file") {
|
|
1317
1400
|
selector = 'input[type="file"]';
|
|
1318
1401
|
} else if (el.type === "range") {
|
|
1319
|
-
selector = el.id ?
|
|
1402
|
+
selector = el.id ? idSelector(el.id) : 'input[type="range"]';
|
|
1320
1403
|
} else if (el.type === "checkbox") {
|
|
1321
|
-
selector = el.id ?
|
|
1404
|
+
selector = el.id ? idSelector(el.id) : 'input[type="checkbox"]';
|
|
1322
1405
|
} else if (el.type === "button") {
|
|
1323
1406
|
if (btnTexts[el.text] === 1) {
|
|
1324
1407
|
selector = `text="${el.text}"`;
|
|
@@ -1328,7 +1411,7 @@ async function crawlPage(url, task = noopTask, storageStatePath) {
|
|
|
1328
1411
|
} else if (el.type === "link") {
|
|
1329
1412
|
selector = `text="${el.text}"`;
|
|
1330
1413
|
} else if (el.id) {
|
|
1331
|
-
selector =
|
|
1414
|
+
selector = idSelector(el.id);
|
|
1332
1415
|
} else if (el.name) {
|
|
1333
1416
|
selector = `[name="${el.name}"]`;
|
|
1334
1417
|
} else if (el.placeholder) {
|
|
@@ -1356,12 +1439,12 @@ async function crawlPage(url, task = noopTask, storageStatePath) {
|
|
|
1356
1439
|
});
|
|
1357
1440
|
await browser.close();
|
|
1358
1441
|
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
1359
|
-
crawlCache.set(
|
|
1442
|
+
crawlCache.set(cacheKey, { timestamp: Date.now(), title, elements: [...elements], scrollable });
|
|
1360
1443
|
if (crawlCache.size > CRAWL_CACHE_MAX_ENTRIES) {
|
|
1361
1444
|
const oldest = crawlCache.keys().next().value;
|
|
1362
1445
|
if (oldest !== void 0) crawlCache.delete(oldest);
|
|
1363
1446
|
}
|
|
1364
|
-
return { title, elements };
|
|
1447
|
+
return { title, elements, scrollable };
|
|
1365
1448
|
}
|
|
1366
1449
|
|
|
1367
1450
|
// src/lib/cursor.ts
|
|
@@ -1370,6 +1453,108 @@ var CURSOR_PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAEsBAMAAAB0
|
|
|
1370
1453
|
// src/lib/index.ts
|
|
1371
1454
|
init_ffmpeg2();
|
|
1372
1455
|
|
|
1456
|
+
// src/lib/json-repair.ts
|
|
1457
|
+
function repairUnicodeEscapes(content) {
|
|
1458
|
+
return content.replace(/\\u\{([0-9a-fA-F]{1,6})\}/g, (match, hex) => {
|
|
1459
|
+
const code = Number.parseInt(hex, 16);
|
|
1460
|
+
return code <= 1114111 ? String.fromCodePoint(code) : match;
|
|
1461
|
+
}).replace(/\\u(?![0-9a-fA-F]{4})/g, "\\\\u");
|
|
1462
|
+
}
|
|
1463
|
+
var MAX_SALVAGE_ATTEMPTS = 500;
|
|
1464
|
+
var CLOSERS = { "{": "}", "[": "]" };
|
|
1465
|
+
function salvageTruncatedJson(content) {
|
|
1466
|
+
const stack = [];
|
|
1467
|
+
const cuts = [];
|
|
1468
|
+
let inString = false;
|
|
1469
|
+
let escaped = false;
|
|
1470
|
+
const record = (index, closesContainer) => cuts.push({ index, open: stack.join(""), closesContainer });
|
|
1471
|
+
for (let i = 0; i < content.length; i++) {
|
|
1472
|
+
const ch = content[i];
|
|
1473
|
+
if (inString) {
|
|
1474
|
+
if (escaped) escaped = false;
|
|
1475
|
+
else if (ch === "\\") escaped = true;
|
|
1476
|
+
else if (ch === '"') {
|
|
1477
|
+
inString = false;
|
|
1478
|
+
record(i + 1, false);
|
|
1479
|
+
}
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
if (ch === '"') {
|
|
1483
|
+
inString = true;
|
|
1484
|
+
} else if (ch === "{" || ch === "[") {
|
|
1485
|
+
stack.push(ch);
|
|
1486
|
+
} else if (ch === "}" || ch === "]") {
|
|
1487
|
+
stack.pop();
|
|
1488
|
+
record(i + 1, true);
|
|
1489
|
+
} else if (ch === ",") {
|
|
1490
|
+
record(i, content[i - 1] === "}" || content[i - 1] === "]");
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const tryCut = (cut) => {
|
|
1494
|
+
const head = content.slice(0, cut.index).replace(/[\s,]+$/, "");
|
|
1495
|
+
if (head.endsWith(":")) return null;
|
|
1496
|
+
let closed = head;
|
|
1497
|
+
for (let d = cut.open.length - 1; d >= 0; d--) {
|
|
1498
|
+
closed += CLOSERS[cut.open[d]];
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
JSON.parse(closed);
|
|
1502
|
+
return closed;
|
|
1503
|
+
} catch {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
const attempts = Math.min(cuts.length, MAX_SALVAGE_ATTEMPTS);
|
|
1508
|
+
for (const containerOnly of [true, false]) {
|
|
1509
|
+
for (let n = 0; n < attempts; n++) {
|
|
1510
|
+
const cut = cuts[cuts.length - 1 - n];
|
|
1511
|
+
if (containerOnly && !cut.closesContainer) continue;
|
|
1512
|
+
const salvaged = tryCut(cut);
|
|
1513
|
+
if (salvaged !== null) return salvaged;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
return null;
|
|
1517
|
+
}
|
|
1518
|
+
function extractJsonCandidates(content) {
|
|
1519
|
+
let text = content.trim();
|
|
1520
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)(?:```|$)/);
|
|
1521
|
+
if (fence?.[1]?.trim()) text = fence[1].trim();
|
|
1522
|
+
const start = text.search(/[{[]/);
|
|
1523
|
+
if (start === -1) return [];
|
|
1524
|
+
const closer = text[start] === "{" ? "}" : "]";
|
|
1525
|
+
const end = text.lastIndexOf(closer);
|
|
1526
|
+
const candidates = [text.slice(start)];
|
|
1527
|
+
if (end > start && end < text.length - 1) candidates.unshift(text.slice(start, end + 1));
|
|
1528
|
+
return candidates;
|
|
1529
|
+
}
|
|
1530
|
+
function parseAIJson(content) {
|
|
1531
|
+
const candidates = extractJsonCandidates(content);
|
|
1532
|
+
if (candidates.length === 0) return null;
|
|
1533
|
+
for (const text of candidates) {
|
|
1534
|
+
try {
|
|
1535
|
+
return { value: JSON.parse(text), repaired: false };
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
for (const text of candidates) {
|
|
1540
|
+
try {
|
|
1541
|
+
return { value: JSON.parse(repairUnicodeEscapes(text)), repaired: true };
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
for (const text of candidates) {
|
|
1546
|
+
for (const variant of [repairUnicodeEscapes(text), text]) {
|
|
1547
|
+
const salvaged = salvageTruncatedJson(variant);
|
|
1548
|
+
if (salvaged === null) continue;
|
|
1549
|
+
try {
|
|
1550
|
+
return { value: JSON.parse(salvaged), repaired: true };
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1373
1558
|
// src/lib/prompts.ts
|
|
1374
1559
|
init_utils();
|
|
1375
1560
|
function buildSystemPrompt(maxSteps) {
|
|
@@ -1424,7 +1609,8 @@ EXAMPLES:
|
|
|
1424
1609
|
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
1425
1610
|
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
1426
1611
|
}
|
|
1427
|
-
|
|
1612
|
+
var MIN_USEFUL_SCROLL_PX = 200;
|
|
1613
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps, scrollable) {
|
|
1428
1614
|
const lang = langName ? `
|
|
1429
1615
|
Language: Write ALL captions in ${langName}.` : "";
|
|
1430
1616
|
const purpose = description ? `
|
|
@@ -1462,8 +1648,13 @@ ${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`
|
|
|
1462
1648
|
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
1463
1649
|
}
|
|
1464
1650
|
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
1651
|
+
const cannotScroll = scrollable !== void 0 && scrollable < MIN_USEFUL_SCROLL_PX;
|
|
1652
|
+
const scrolling = cannotScroll ? `
|
|
1653
|
+
|
|
1654
|
+
SCROLLING: this page has only ${scrollable}px of scroll travel at the recording size, so it does not scroll.` : "";
|
|
1655
|
+
const scrollRule = cannotScroll ? "\n\u2022 NEVER use the scroll action: this page does not scroll, so the video would freeze. Use wait and click steps only" : "";
|
|
1465
1656
|
return `URL: ${url}
|
|
1466
|
-
Title: ${title}${purpose}
|
|
1657
|
+
Title: ${title}${purpose}${scrolling}
|
|
1467
1658
|
|
|
1468
1659
|
AVAILABLE ELEMENTS (grouped by action):
|
|
1469
1660
|
${elementSummary}
|
|
@@ -1473,7 +1664,7 @@ REQUIREMENTS:
|
|
|
1473
1664
|
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
1474
1665
|
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
1475
1666
|
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
1476
|
-
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
1667
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${scrollRule}${lang}
|
|
1477
1668
|
|
|
1478
1669
|
Return JSON:
|
|
1479
1670
|
{
|
|
@@ -1552,7 +1743,7 @@ function generateASSHeader(width, height, font, size, primaryColor, secondaryCol
|
|
|
1552
1743
|
async function registerFreeUser(email) {
|
|
1553
1744
|
const APP_ID2 = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
1554
1745
|
try {
|
|
1555
|
-
const response = await fetch(`https://
|
|
1746
|
+
const response = await fetch(`https://sparkpay.dev/api/register-free`, {
|
|
1556
1747
|
method: "POST",
|
|
1557
1748
|
headers: { "Content-Type": "application/json" },
|
|
1558
1749
|
body: JSON.stringify({ email, app_id: APP_ID2, app_name: "QuickPeek" })
|
|
@@ -1636,11 +1827,27 @@ init_helpers();
|
|
|
1636
1827
|
// src/lib/tts/synthesize.ts
|
|
1637
1828
|
init_helpers();
|
|
1638
1829
|
import { exec } from "child_process";
|
|
1639
|
-
import {
|
|
1830
|
+
import { createWriteStream } from "fs";
|
|
1831
|
+
import { stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
|
|
1640
1832
|
import { join as join4, resolve as resolve2 } from "path";
|
|
1833
|
+
import { pipeline } from "stream/promises";
|
|
1641
1834
|
import { promisify } from "util";
|
|
1642
1835
|
var execAsync = promisify(exec);
|
|
1836
|
+
var PYTHON_ATTEMPTS = 2;
|
|
1643
1837
|
async function synthesizeSpeech(text, outputPath, voice) {
|
|
1838
|
+
let pythonMsg = "";
|
|
1839
|
+
for (let attempt = 0; attempt < PYTHON_ATTEMPTS; attempt++) {
|
|
1840
|
+
try {
|
|
1841
|
+
await synthesizeWithPython(text, outputPath, voice);
|
|
1842
|
+
return;
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
pythonMsg = err instanceof Error ? err.message : String(err);
|
|
1845
|
+
if (attempt < PYTHON_ATTEMPTS - 1) await new Promise((r) => setTimeout(r, 500));
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
await synthesizeWithNode(text, outputPath, voice, pythonMsg);
|
|
1849
|
+
}
|
|
1850
|
+
async function synthesizeWithPython(text, outputPath, voice) {
|
|
1644
1851
|
const tempScriptPath = join4(resolve2(outputPath, ".."), `tts-${Date.now()}.py`);
|
|
1645
1852
|
const wslScriptPath = isWindows ? toWSLPathForTTS(resolve2(tempScriptPath)) : tempScriptPath;
|
|
1646
1853
|
const wslOutputPath = isWindows ? toWSLPathForTTS(resolve2(outputPath)) : outputPath;
|
|
@@ -1661,28 +1868,30 @@ asyncio.run(main())
|
|
|
1661
1868
|
await writeFile2(tempScriptPath, pythonScript);
|
|
1662
1869
|
const pythonCmd = isWindows ? `wsl python3 "${wslScriptPath}"` : `python3 "${wslScriptPath}"`;
|
|
1663
1870
|
await execAsync(pythonCmd, { timeout: 3e4 });
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1871
|
+
} finally {
|
|
1872
|
+
await unlink2(tempScriptPath).catch(() => {
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function synthesizeWithNode(text, outputPath, voice, pythonMsg, maxRetries = 2) {
|
|
1877
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1878
|
+
try {
|
|
1879
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
|
|
1880
|
+
const tts = new MsEdgeTTS();
|
|
1881
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3);
|
|
1882
|
+
const { audioStream } = tts.toStream(text);
|
|
1883
|
+
await pipeline(audioStream, createWriteStream(outputPath));
|
|
1884
|
+
const { size } = await stat(outputPath);
|
|
1885
|
+
if (size === 0) throw new Error("No audio data received");
|
|
1886
|
+
return;
|
|
1887
|
+
} catch (nodeErr) {
|
|
1888
|
+
await unlink2(outputPath).catch(() => {
|
|
1889
|
+
});
|
|
1890
|
+
if (attempt === maxRetries) {
|
|
1891
|
+
const nodeMsg = nodeErr instanceof Error ? nodeErr.message : String(nodeErr);
|
|
1892
|
+
throw new Error(`TTS failed (Python: ${pythonMsg.slice(0, 100)}, Node: ${nodeMsg.slice(0, 100)})`);
|
|
1685
1893
|
}
|
|
1894
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1686
1895
|
}
|
|
1687
1896
|
}
|
|
1688
1897
|
}
|
|
@@ -1794,6 +2003,7 @@ init_timing();
|
|
|
1794
2003
|
init_utils();
|
|
1795
2004
|
init_watermark();
|
|
1796
2005
|
export {
|
|
2006
|
+
CLI_BACKOFF_MS,
|
|
1797
2007
|
CONFIG_FILE,
|
|
1798
2008
|
CURSOR_CSS,
|
|
1799
2009
|
CURSOR_PNG,
|
|
@@ -1804,9 +2014,11 @@ export {
|
|
|
1804
2014
|
FEATURES,
|
|
1805
2015
|
INTERACTIVE_SELECTORS,
|
|
1806
2016
|
WIDE_VOICES as LANGUAGE_VOICES,
|
|
2017
|
+
SERVER_BACKOFF_MS,
|
|
1807
2018
|
VERSION,
|
|
1808
2019
|
alignVoiceoverToRecording,
|
|
1809
2020
|
applyTransitions,
|
|
2021
|
+
buildConcatArgs,
|
|
1810
2022
|
buildKeepSegments,
|
|
1811
2023
|
buildStepTransitions,
|
|
1812
2024
|
buildSystemPrompt,
|
|
@@ -1820,9 +2032,11 @@ export {
|
|
|
1820
2032
|
concatVideoWithOutro,
|
|
1821
2033
|
crawlPage,
|
|
1822
2034
|
estimateWordTimings,
|
|
2035
|
+
extractJsonCandidates,
|
|
1823
2036
|
generateASSHeader,
|
|
1824
2037
|
generateSilence,
|
|
1825
2038
|
generateVoiceover,
|
|
2039
|
+
getAudioCodec,
|
|
1826
2040
|
getErrorMessage,
|
|
1827
2041
|
getLanguageName,
|
|
1828
2042
|
getMediaDuration,
|
|
@@ -1833,20 +2047,25 @@ export {
|
|
|
1833
2047
|
hasAccess,
|
|
1834
2048
|
hasTrialRemaining,
|
|
1835
2049
|
hexToAss as hexToASS,
|
|
2050
|
+
idSelector,
|
|
1836
2051
|
isCanceling,
|
|
1837
2052
|
isExcludedLink,
|
|
2053
|
+
isHonestWav,
|
|
1838
2054
|
isPaid,
|
|
1839
2055
|
isVerified,
|
|
1840
2056
|
loadExistingVoiceover,
|
|
1841
2057
|
loadVoiceoverMeta,
|
|
1842
2058
|
mapTimestampsAfterCompression,
|
|
1843
2059
|
normalizeUrl,
|
|
2060
|
+
parseAIJson,
|
|
1844
2061
|
parseAIPlanResponse,
|
|
1845
2062
|
parsePartial,
|
|
1846
2063
|
pricingUrl,
|
|
1847
2064
|
registerFreeUser,
|
|
2065
|
+
repairUnicodeEscapes,
|
|
1848
2066
|
resolveVoice,
|
|
1849
2067
|
rgbaToHex,
|
|
2068
|
+
salvageTruncatedJson,
|
|
1850
2069
|
shouldSkipLink,
|
|
1851
2070
|
shouldWatermark,
|
|
1852
2071
|
showPaywall,
|